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}