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.contextmenu;
029
030import org.opencms.main.CmsLog;
031import org.opencms.ui.I_CmsDialogContext;
032import org.opencms.ui.actions.CmsContextMenuActionItem;
033import org.opencms.ui.actions.I_CmsDefaultAction;
034import org.opencms.util.CmsTreeNode;
035
036import java.util.ArrayList;
037import java.util.Collection;
038import java.util.Collections;
039import java.util.Comparator;
040import java.util.IdentityHashMap;
041import java.util.Iterator;
042import java.util.LinkedHashMap;
043import java.util.List;
044import java.util.Map;
045import java.util.Set;
046
047import org.apache.commons.logging.Log;
048
049import com.google.common.collect.Lists;
050import com.google.common.collect.Maps;
051import com.google.common.collect.Sets;
052
053/**
054 * Helper class for building context menus from the list of available context menu items.<p>
055 */
056public class CmsContextMenuTreeBuilder {
057
058    /** The logger instance for this class. */
059    private static final Log LOG = CmsLog.getLog(CmsContextMenuTreeBuilder.class);
060
061    /** The dialog context. */
062    private I_CmsDialogContext m_context;
063
064    /** The default action item. */
065    private I_CmsContextMenuItem m_defaultActionItem;
066
067    /** Cached visibilities for context menu entries. */
068    private IdentityHashMap<I_CmsContextMenuItem, CmsMenuItemVisibilityMode> m_visiblities = new IdentityHashMap<I_CmsContextMenuItem, CmsMenuItemVisibilityMode>();
069
070    /**
071     * Creates a new instance.<p>
072     *
073     * @param context the dialog context
074     */
075    public CmsContextMenuTreeBuilder(I_CmsDialogContext context) {
076        m_context = context;
077    }
078
079    /**
080     * Builds the complete context menu from the given available items.<p>
081     *
082     * @param availableItems the available items
083     *
084     * @return the complete context menu
085     */
086    public CmsTreeNode<I_CmsContextMenuItem> buildAll(List<I_CmsContextMenuItem> availableItems) {
087
088        CmsTreeNode<I_CmsContextMenuItem> result = buildTree(availableItems);
089        removeEmptySubtrees(result);
090        return result;
091
092    }
093
094    /**
095     * Builds a tree from a list of available context menu items.<p>
096     *
097     * The root node of the returned tree has no useful data, its child nodes correspond to the top-level
098     * entries of the ccontext menu.
099     *
100     * @param items the available context menu items
101     * @return the context menu item tree
102     */
103    public CmsTreeNode<I_CmsContextMenuItem> buildTree(List<I_CmsContextMenuItem> items) {
104
105        items = new ArrayList<I_CmsContextMenuItem>(items);
106
107        // First sort by priority and then use a map with the id as the key to store the items,
108        // eliminating items with the same id but a lower priority than another item
109
110        Collections.sort(items, new Comparator<I_CmsContextMenuItem>() {
111
112            public int compare(I_CmsContextMenuItem a, I_CmsContextMenuItem b) {
113
114                return Integer.compare(a.getPriority(), b.getPriority());
115            }
116        });
117        LinkedHashMap<String, I_CmsContextMenuItem> itemsById = Maps.newLinkedHashMap();
118        for (I_CmsContextMenuItem item : items) {
119            String id = item.getId();
120            I_CmsContextMenuItem prevItem = itemsById.get(id);
121            if (prevItem != null) {
122                LOG.info(
123                    "Discarding overridden context menu item " + prevItem + " because of higher priority item " + item);
124            }
125            itemsById.put(id, item);
126        }
127
128        // Now sort by order. Since all children of a node should be processed in one iteration of the following loop,
129        // this order also applies to the child order of each tree node built in the next step
130        List<I_CmsContextMenuItem> uniqueItems = Lists.newArrayList(itemsById.values());
131        uniqueItems = filterVisible(uniqueItems);
132        if (m_context.getResources().size() == 1) {
133            m_defaultActionItem = findDefaultAction(uniqueItems);
134        }
135
136        Collections.sort(uniqueItems, new Comparator<I_CmsContextMenuItem>() {
137
138            public int compare(I_CmsContextMenuItem a, I_CmsContextMenuItem b) {
139
140                return Float.compare(a.getOrder(), b.getOrder());
141            }
142        });
143        Set<String> processedIds = Sets.newHashSet();
144        boolean changed = true;
145        Map<String, CmsTreeNode<I_CmsContextMenuItem>> treesById = Maps.newHashMap();
146
147        // Create childless tree node for each item
148        for (I_CmsContextMenuItem item : itemsById.values()) {
149            CmsTreeNode<I_CmsContextMenuItem> node = new CmsTreeNode<I_CmsContextMenuItem>();
150            node.setData(item);
151            treesById.put(item.getId(), node);
152        }
153        CmsTreeNode<I_CmsContextMenuItem> root = new CmsTreeNode<I_CmsContextMenuItem>();
154
155        // Use null as the root node, which does not have any useful data
156        treesById.put(null, root);
157
158        // Iterate through list multiple times, each time only processing those items whose parents
159        // we have encountered in a previous iteration (actually, in the last iteration). We do this so that the resulting
160        // tree is actually a tree and contains no cycles, even if there is a reference cycle between the context menu items via their parent ids.
161        // (Items which form such a cycle will never be reached.)
162        while (changed) {
163            changed = false;
164            Iterator<I_CmsContextMenuItem> iterator = uniqueItems.iterator();
165            Set<String> currentLevel = Sets.newHashSet();
166            while (iterator.hasNext()) {
167                I_CmsContextMenuItem currentItem = iterator.next();
168                String parentId = currentItem.getParentId();
169                if ((parentId == null) || processedIds.contains(parentId)) {
170                    changed = true;
171                    iterator.remove();
172                    currentLevel.add(currentItem.getId());
173                    treesById.get(parentId).addChild(treesById.get(currentItem.getId()));
174                }
175            }
176            processedIds.addAll(currentLevel);
177        }
178        return root;
179    }
180
181    /**
182     * Filters out invisible context menu items from a given list.<p>
183     *
184     * @param items the items
185     *
186     * @return the list of context menu items
187     */
188    public List<I_CmsContextMenuItem> filterVisible(List<I_CmsContextMenuItem> items) {
189
190        List<I_CmsContextMenuItem> result = Lists.newArrayList();
191        for (I_CmsContextMenuItem item : items) {
192            CmsMenuItemVisibilityMode visibility = getVisibility(item);
193            if (!visibility.isInVisible()) {
194                result.add(item);
195            }
196        }
197        return result;
198    }
199
200    /**
201     * Returns the default action item if available.<p>
202     * Only available once {@link #buildTree(List)} or {@link #buildAll(List)} has been executed.<p>
203     *
204     * @return the default action item
205     */
206    public I_CmsContextMenuItem getDefaultActionItem() {
207
208        return m_defaultActionItem;
209    }
210
211    /**
212     * Gets the visibility for a given item (cached, if possible).<p>
213     *
214     * @param item the item
215     * @return the visibility of that item
216     */
217    public CmsMenuItemVisibilityMode getVisibility(I_CmsContextMenuItem item) {
218
219        CmsMenuItemVisibilityMode result = m_visiblities.get(item);
220        if (result == null) {
221            result = item.getVisibility(m_context);
222            m_visiblities.put(item, result);
223        }
224        return result;
225    }
226
227    /**
228     * Recursively remove subtrees (destructively!) which do not contain any 'leaf' context menu items.<p>
229     *
230     * @param root the root of the tree to process
231     */
232    public void removeEmptySubtrees(CmsTreeNode<I_CmsContextMenuItem> root) {
233
234        List<CmsTreeNode<I_CmsContextMenuItem>> children = root.getChildren();
235        if ((root.getData() != null) && root.getData().isLeafItem()) {
236            children.clear();
237        } else {
238            Iterator<CmsTreeNode<I_CmsContextMenuItem>> iter = children.iterator();
239            while (iter.hasNext()) {
240                CmsTreeNode<I_CmsContextMenuItem> node = iter.next();
241                removeEmptySubtrees(node);
242                if ((node.getData() != null) && !node.getData().isLeafItem() && (node.getChildren().size() == 0)) {
243                    iter.remove();
244                }
245            }
246        }
247    }
248
249    /**
250     * Evaluates the default action if any for highlighting within the menu.<p>
251     *
252     * @param items the menu items
253     *
254     * @return the default action if available
255     */
256    private I_CmsContextMenuItem findDefaultAction(Collection<I_CmsContextMenuItem> items) {
257
258        I_CmsContextMenuItem result = null;
259        int resultRank = -1;
260        for (I_CmsContextMenuItem menuItem : items) {
261            if ((menuItem instanceof CmsContextMenuActionItem)
262                && (((CmsContextMenuActionItem)menuItem).getWorkplaceAction() instanceof I_CmsDefaultAction)) {
263                I_CmsDefaultAction action = (I_CmsDefaultAction)((CmsContextMenuActionItem)menuItem).getWorkplaceAction();
264                if (getVisibility(menuItem).isActive()) {
265                    if (result == null) {
266                        result = menuItem;
267                        resultRank = action.getDefaultActionRank(m_context);
268                    } else {
269                        int rank = action.getDefaultActionRank(m_context);
270                        if (rank > resultRank) {
271                            result = menuItem;
272                            resultRank = rank;
273                        }
274                    }
275                }
276            }
277        }
278        return result;
279    }
280
281}