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}