001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.ui.apps; 029 030import org.opencms.main.CmsLog; 031import org.opencms.util.CmsStringUtil; 032 033import java.util.Collections; 034import java.util.Comparator; 035import java.util.HashSet; 036import java.util.List; 037import java.util.Map; 038import java.util.Set; 039 040import org.apache.commons.logging.Log; 041 042import com.google.common.collect.ComparisonChain; 043import com.google.common.collect.Lists; 044import com.google.common.collect.Maps; 045import com.google.common.collect.Sets; 046 047/** 048 * Helper class for building a tree of categories/apps which should be displayed from the list of available apps and categories.<p> 049 */ 050public class CmsAppHierarchyBuilder { 051 052 /** The logger instance for this tree. */ 053 private static final Log LOG = CmsLog.getLog(CmsAppHierarchyBuilder.class); 054 055 /** The list of available categories. */ 056 private List<I_CmsAppCategory> m_appCategoryList = Lists.newArrayList(); 057 058 /** The list of available app configurations. */ 059 private List<I_CmsWorkplaceAppConfiguration> m_appConfigs = Lists.newArrayList(); 060 061 /** The working set of category tree nodes. */ 062 private Map<String, CmsAppCategoryNode> m_nodes = Maps.newHashMap(); 063 064 /** The root tree node. */ 065 private CmsAppCategoryNode m_rootNode = new CmsAppCategoryNode(); 066 067 /** 068 * Adds an app configuration.<p> 069 * 070 * @param appConfig the app configuration to add 071 */ 072 public void addAppConfiguration(I_CmsWorkplaceAppConfiguration appConfig) { 073 074 m_appConfigs.add(appConfig); 075 } 076 077 /** 078 * Adds an app category.<p> 079 * 080 * @param category the app category to add 081 */ 082 public void addCategory(I_CmsAppCategory category) { 083 084 m_appCategoryList.add(category); 085 } 086 087 /** 088 * Builds the tree of categories and apps.<p> 089 * 090 * This tree will only include those categories which are reachable by following the parent chain of 091 * an available app configuration up to the root category (null). 092 * 093 * @return the root node of the tree 094 */ 095 public CmsAppCategoryNode buildHierarchy() { 096 097 // STEP 0: Initialize everything and sort categories by priority 098 099 Collections.sort(m_appCategoryList, new Comparator<I_CmsAppCategory>() { 100 101 public int compare(I_CmsAppCategory cat1, I_CmsAppCategory cat2) { 102 103 return ComparisonChain.start().compare(cat1.getPriority(), cat2.getPriority()).result(); 104 } 105 }); 106 m_rootNode = new CmsAppCategoryNode(); 107 m_nodes.clear(); 108 m_nodes.put(null, m_rootNode); 109 110 // STEP 1: Create a node for each category 111 112 for (I_CmsAppCategory category : m_appCategoryList) { 113 m_nodes.put(category.getId(), new CmsAppCategoryNode(category)); 114 } 115 116 // STEP 2: Assign category nodes to nodes for their parent category 117 118 for (CmsAppCategoryNode node : m_nodes.values()) { 119 if (node != m_rootNode) { 120 addNodeToItsParent(node); 121 } 122 } 123 124 // STEP 3: Assign app configs to category nodes 125 126 for (I_CmsWorkplaceAppConfiguration appConfig : m_appConfigs) { 127 addAppConfigToCategory(appConfig); 128 } 129 130 // STEP 4: Validate whether there are unused categories / apps 131 132 Set<String> usedNodes = findReachableNodes(m_rootNode, new HashSet<String>()); 133 if (usedNodes.size() < m_nodes.size()) { 134 LOG.warn("Unused app categories: " + Sets.difference(m_nodes.keySet(), usedNodes)); 135 } 136 Set<String> unusedApps = Sets.newHashSet(); 137 for (I_CmsWorkplaceAppConfiguration appConfig : m_appConfigs) { 138 if (!usedNodes.contains(appConfig.getAppCategory())) { 139 unusedApps.add(appConfig.getId()); 140 } 141 } 142 if (unusedApps.size() > 0) { 143 LOG.warn("Unused apps: " + unusedApps); 144 } 145 146 // STEP 5: Remove parts of the hierarchy which don't contain any apps 147 m_rootNode.removeApplessSubtrees(); 148 149 // STEP 6: Sort all categories and app configurations for each node 150 m_rootNode.sortRecursively(); 151 152 return m_rootNode; 153 } 154 155 /** 156 * Gets the root node.<p> 157 * 158 * @return the root node 159 */ 160 public CmsAppCategoryNode getRootNode() { 161 162 return m_rootNode; 163 } 164 165 /** 166 * Adds an app configuration to the node belonging to its parent category id.<p> 167 * 168 * @param appConfig the app configuration to add to its parent node 169 */ 170 protected void addAppConfigToCategory(I_CmsWorkplaceAppConfiguration appConfig) { 171 172 CmsAppCategoryNode node = m_nodes.get(appConfig.getAppCategory()); 173 if (node == null) { 174 LOG.info( 175 "Missing parent [" 176 + appConfig.getAppCategory() 177 + "] for " 178 + appConfig.getId() 179 + " / " 180 + appConfig.getClass().getName()); 181 } else { 182 node.addAppConfiguration(appConfig); 183 } 184 } 185 186 /** 187 * Adds a category node to the category node belonging to its parent id.<p> 188 * 189 * @param node the node which should be attached to its parent 190 */ 191 protected void addNodeToItsParent(CmsAppCategoryNode node) { 192 193 String parentId = node.getCategory().getParentId(); 194 if (CmsStringUtil.isEmptyOrWhitespaceOnly(parentId)) { 195 parentId = null; 196 } 197 CmsAppCategoryNode parentNode = m_nodes.get(parentId); 198 if (parentNode == null) { 199 LOG.error( 200 "Missing parent [" + node.getCategory().getParentId() + "] for [" + node.getCategory().getId() + "]"); 201 } else { 202 parentNode.addChild(node); 203 } 204 } 205 206 /** 207 * Finds the category nodes reachable from a node.<p> 208 * 209 * @param rootNode the root node 210 * @param reachableNodes set used for collecting the reachable nodes 211 * 212 * @return the set of reachable node ids 213 */ 214 private Set<String> findReachableNodes(CmsAppCategoryNode rootNode, HashSet<String> reachableNodes) { 215 216 reachableNodes.add(rootNode.getCategory().getId()); 217 for (CmsAppCategoryNode child : rootNode.getChildren()) { 218 findReachableNodes(child, reachableNodes); 219 } 220 return reachableNodes; 221 } 222 223}