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.List;
043import java.util.Map;
044import java.util.Set;
045import java.util.stream.Collectors;
046
047import org.apache.commons.logging.Log;
048
049import com.google.common.collect.Maps;
050import com.google.common.collect.Sets;
051
052/**
053 * Helper class for building context menus from the list of available context menu items.<p>
054 */
055public class CmsContextMenuTreeBuilder {
056
057    /** The logger instance for this class. */
058    private static final Log LOG = CmsLog.getLog(CmsContextMenuTreeBuilder.class);
059
060    /** The dialog context. */
061    private I_CmsDialogContext m_context;
062
063    /** The default action item. */
064    private I_CmsContextMenuItem m_defaultActionItem;
065
066    /** Cached visibilities for context menu entries. */
067    private IdentityHashMap<I_CmsContextMenuItem, CmsMenuItemVisibilityMode> m_visiblities = new IdentityHashMap<I_CmsContextMenuItem, CmsMenuItemVisibilityMode>();
068
069    /**
070     * Creates a new instance.<p>
071     *
072     * @param context the dialog context
073     */
074    public CmsContextMenuTreeBuilder(I_CmsDialogContext context) {
075
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        Map<String, List<I_CmsContextMenuItem>> itemsById = items.stream().collect(
108            Collectors.groupingBy(item -> item.getId()));
109        List<I_CmsContextMenuItem> uniqueItems = itemsById.values().stream().map(itemsForCurrentId -> {
110            Collections.sort(itemsForCurrentId, Comparator.comparing(item -> -item.getPriority())); // highest priority first
111            int i;
112            for (i = 0; i < (itemsForCurrentId.size() - 1); i++) { // the last entry is *not* part of the iteration
113                CmsMenuItemVisibilityMode visibility = getVisibility(itemsForCurrentId.get(i));
114                if (!visibility.isUseNext()) {
115                    break;
116                }
117            }
118            return itemsForCurrentId.get(i); // i is the first index that didn't return USE_NEXT (it may be the index of the last item, which we didn't ask for its visibility)
119        }).filter(item -> !getVisibility(item).isInVisible()).collect(Collectors.toList());
120
121        if (m_context.getResources().size() == 1) {
122            m_defaultActionItem = findDefaultAction(uniqueItems);
123        }
124
125        // Now sort by order. Since all children of a node should be processed in one iteration of the following loop,
126        // this order also applies to the child order of each tree node built in the next step
127
128        Collections.sort(uniqueItems, new Comparator<I_CmsContextMenuItem>() {
129
130            public int compare(I_CmsContextMenuItem a, I_CmsContextMenuItem b) {
131
132                return Float.compare(a.getOrder(), b.getOrder());
133            }
134        });
135        Set<String> processedIds = Sets.newHashSet();
136        boolean changed = true;
137        Map<String, CmsTreeNode<I_CmsContextMenuItem>> treesById = Maps.newHashMap();
138
139        // Create childless tree node for each item
140        for (I_CmsContextMenuItem item : uniqueItems) {
141            CmsTreeNode<I_CmsContextMenuItem> node = new CmsTreeNode<I_CmsContextMenuItem>();
142            node.setData(item);
143            treesById.put(item.getId(), node);
144        }
145        CmsTreeNode<I_CmsContextMenuItem> root = new CmsTreeNode<I_CmsContextMenuItem>();
146
147        // Use null as the root node, which does not have any useful data
148        treesById.put(null, root);
149
150        // Iterate through list multiple times, each time only processing those items whose parents
151        // we have encountered in a previous iteration (actually, in the last iteration). We do this so that the resulting
152        // 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.
153        // (Items which form such a cycle will never be reached.)
154        while (changed) {
155            changed = false;
156            Iterator<I_CmsContextMenuItem> iterator = uniqueItems.iterator();
157            Set<String> currentLevel = Sets.newHashSet();
158            while (iterator.hasNext()) {
159                I_CmsContextMenuItem currentItem = iterator.next();
160                String parentId = currentItem.getParentId();
161                if ((parentId == null) || processedIds.contains(parentId)) {
162                    changed = true;
163                    iterator.remove();
164                    currentLevel.add(currentItem.getId());
165                    treesById.get(parentId).addChild(treesById.get(currentItem.getId()));
166                }
167            }
168            processedIds.addAll(currentLevel);
169        }
170        return root;
171    }
172
173    /**
174     * Returns the default action item if available.<p>
175     * Only available once {@link #buildTree(List)} or {@link #buildAll(List)} has been executed.<p>
176     *
177     * @return the default action item
178     */
179    public I_CmsContextMenuItem getDefaultActionItem() {
180
181        return m_defaultActionItem;
182    }
183
184    /**
185     * Gets the visibility for a given item (cached, if possible).<p>
186     *
187     * @param item the item
188     * @return the visibility of that item
189     */
190    public CmsMenuItemVisibilityMode getVisibility(I_CmsContextMenuItem item) {
191
192        CmsMenuItemVisibilityMode result = m_visiblities.get(item);
193        if (result == null) {
194            result = item.getVisibility(m_context);
195            m_visiblities.put(item, result);
196        }
197        return result;
198    }
199
200    /**
201     * Recursively remove subtrees (destructively!) which do not contain any 'leaf' context menu items.<p>
202     *
203     * @param root the root of the tree to process
204     */
205    public void removeEmptySubtrees(CmsTreeNode<I_CmsContextMenuItem> root) {
206
207        List<CmsTreeNode<I_CmsContextMenuItem>> children = root.getChildren();
208        if ((root.getData() != null) && root.getData().isLeafItem()) {
209            children.clear();
210        } else {
211            Iterator<CmsTreeNode<I_CmsContextMenuItem>> iter = children.iterator();
212            while (iter.hasNext()) {
213                CmsTreeNode<I_CmsContextMenuItem> node = iter.next();
214                removeEmptySubtrees(node);
215                if ((node.getData() != null) && !node.getData().isLeafItem() && (node.getChildren().size() == 0)) {
216                    iter.remove();
217                }
218            }
219        }
220    }
221
222    /**
223     * Evaluates the default action if any for highlighting within the menu.<p>
224     *
225     * @param items the menu items
226     *
227     * @return the default action if available
228     */
229    private I_CmsContextMenuItem findDefaultAction(Collection<I_CmsContextMenuItem> items) {
230
231        I_CmsContextMenuItem result = null;
232        int resultRank = -1;
233        for (I_CmsContextMenuItem menuItem : items) {
234            if ((menuItem instanceof CmsContextMenuActionItem)
235                && (((CmsContextMenuActionItem)menuItem).getWorkplaceAction() instanceof I_CmsDefaultAction)) {
236                I_CmsDefaultAction action = (I_CmsDefaultAction)((CmsContextMenuActionItem)menuItem).getWorkplaceAction();
237                if (getVisibility(menuItem).isActive()) {
238                    if (result == null) {
239                        result = menuItem;
240                        resultRank = action.getDefaultActionRank(m_context);
241                    } else {
242                        int rank = action.getDefaultActionRank(m_context);
243                        if (rank > resultRank) {
244                            result = menuItem;
245                            resultRank = rank;
246                        }
247                    }
248                }
249            }
250        }
251        return result;
252    }
253
254}