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}