001/* 002 * (C) Copyright 2006-2007 Nuxeo SAS (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Nuxeo - initial API and implementation 016 * 017 * $Id$ 018 */ 019package org.nuxeo.ecm.webapp.tree; 020 021import static org.jboss.seam.ScopeType.CONVERSATION; 022import static org.jboss.seam.annotations.Install.FRAMEWORK; 023 024import java.io.IOException; 025import java.io.Serializable; 026import java.util.ArrayList; 027import java.util.HashMap; 028import java.util.List; 029import java.util.Map; 030 031import javax.faces.context.ExternalContext; 032import javax.faces.context.FacesContext; 033import javax.servlet.http.HttpServletResponse; 034 035import org.apache.commons.lang.StringUtils; 036import org.apache.commons.logging.Log; 037import org.apache.commons.logging.LogFactory; 038import org.jboss.seam.Component; 039import org.jboss.seam.annotations.In; 040import org.jboss.seam.annotations.Install; 041import org.jboss.seam.annotations.Name; 042import org.jboss.seam.annotations.Observer; 043import org.jboss.seam.annotations.Scope; 044import org.jboss.seam.annotations.intercept.BypassInterceptors; 045import org.nuxeo.common.utils.Path; 046import org.nuxeo.ecm.core.api.CoreSession; 047import org.nuxeo.ecm.core.api.DocumentModel; 048import org.nuxeo.ecm.core.api.Filter; 049import org.nuxeo.ecm.core.api.PathRef; 050import org.nuxeo.ecm.core.api.Sorter; 051import org.nuxeo.ecm.platform.ui.web.api.NavigationContext; 052import org.nuxeo.ecm.webapp.helpers.EventNames; 053import org.nuxeo.runtime.api.Framework; 054import org.richfaces.event.CollapsibleSubTableToggleEvent; 055 056/** 057 * Manages the navigation tree. 058 * 059 * @author Razvan Caraghin 060 * @author Anahide Tchertchian 061 */ 062@Scope(CONVERSATION) 063@Name("treeActions") 064@Install(precedence = FRAMEWORK) 065public class TreeActionsBean implements TreeActions, Serializable { 066 067 private static final long serialVersionUID = 1L; 068 069 private static final Log log = LogFactory.getLog(TreeActionsBean.class); 070 071 public static final String NODE_SELECTED_MARKER = TreeActionsBean.class.getName() + "_NODE_SELECTED_MARKER"; 072 073 @In(create = true, required = false) 074 protected transient CoreSession documentManager; 075 076 @In(create = true) 077 protected transient NavigationContext navigationContext; 078 079 protected Map<String, List<DocumentTreeNode>> trees = new HashMap<String, List<DocumentTreeNode>>(); 080 081 protected String currentDocumentPath; 082 083 @In(create = true, required = false) 084 protected Boolean isUserWorkspace; 085 086 @In(create = true, required = false) 087 protected String currentPersonalWorkspacePath; 088 089 protected String userWorkspacePath; 090 091 // cache the path of the tree root to check if invalidation are needed when 092 // bypassing interceptors 093 protected String firstAccessibleParentPath; 094 095 protected boolean showingGlobalRoot; 096 097 @In(create = true) 098 protected TreeInvalidatorBean treeInvalidator; 099 100 public List<DocumentTreeNode> getTreeRoots() { 101 return getTreeRoots(false); 102 } 103 104 public List<DocumentTreeNode> getTreeRoots(String treeName) { 105 return getTreeRoots(false, treeName); 106 } 107 108 protected List<DocumentTreeNode> getTreeRoots(boolean showRoot, String treeName) { 109 return getTreeRoots(showRoot, navigationContext.getCurrentDocument(), treeName); 110 } 111 112 protected List<DocumentTreeNode> getTreeRoots(boolean showRoot) { 113 return getTreeRoots(showRoot, navigationContext.getCurrentDocument(), DEFAULT_TREE_PLUGIN_NAME); 114 } 115 116 protected List<DocumentTreeNode> getTreeRoots(boolean showRoot, DocumentModel currentDocument) 117 { 118 return getTreeRoots(showRoot, currentDocument, DEFAULT_TREE_PLUGIN_NAME); 119 } 120 121 /** 122 * @since 5.4 123 */ 124 protected List<DocumentTreeNode> getTreeRoots(boolean showRoot, DocumentModel currentDocument, String treeName) 125 { 126 127 if (treeInvalidator.needsInvalidation()) { 128 reset(); 129 treeInvalidator.invalidationDone(); 130 } 131 if (Boolean.TRUE.equals(isUserWorkspace)) { 132 userWorkspacePath = getUserWorkspacePath(); 133 } 134 List<DocumentTreeNode> currentTree = trees.get(treeName); 135 if (currentTree == null) { 136 currentTree = new ArrayList<DocumentTreeNode>(); 137 DocumentModel globalRoot = null; 138 DocumentModel firstAccessibleParent = null; 139 if (currentDocument != null) { 140 141 if (Boolean.TRUE.equals(isUserWorkspace)) { 142 firstAccessibleParent = documentManager.getDocument(new PathRef(userWorkspacePath)); 143 } else { 144 145 List<DocumentModel> parents = documentManager.getParentDocuments(currentDocument.getRef()); 146 if (!parents.isEmpty()) { 147 firstAccessibleParent = parents.get(0); 148 } else if (!"Root".equals(currentDocument.getType()) && currentDocument.isFolder()) { 149 // default on current doc 150 firstAccessibleParent = currentDocument; 151 } else { 152 if (showRoot) { 153 firstAccessibleParent = currentDocument; 154 } 155 } 156 157 } 158 if (showRoot 159 && (firstAccessibleParent == null || !"/".equals(firstAccessibleParent.getPathAsString()))) { 160 // also add the global root if we don't already show it and it's accessible 161 if (documentManager.exists(new PathRef("/"))) { 162 globalRoot = documentManager.getRootDocument(); 163 } 164 } 165 } 166 showingGlobalRoot = globalRoot != null; 167 if (showingGlobalRoot) { 168 DocumentTreeNode treeRoot = newDocumentTreeNode(globalRoot, treeName); 169 currentTree.add(treeRoot); 170 log.debug("Tree initialized with additional global root"); 171 } 172 firstAccessibleParentPath = firstAccessibleParent == null ? null : firstAccessibleParent.getPathAsString(); 173 if (firstAccessibleParent != null) { 174 DocumentTreeNode treeRoot = newDocumentTreeNode(firstAccessibleParent, treeName); 175 currentTree.add(treeRoot); 176 log.debug("Tree initialized with document: " + firstAccessibleParent.getId()); 177 } else { 178 log.debug("Could not initialize the navigation tree: no parent" + " found for current document"); 179 } 180 trees.put(treeName, currentTree); 181 } 182 return trees.get(treeName); 183 } 184 185 protected DocumentTreeNode newDocumentTreeNode(DocumentModel doc, String treeName) { 186 TreeManager treeManager = Framework.getService(TreeManager.class); 187 Filter filter = treeManager.getFilter(treeName); 188 Filter leafFilter = treeManager.getLeafFilter(treeName); 189 Sorter sorter = treeManager.getSorter(treeName); 190 String pageProvider = treeManager.getPageProviderName(treeName); 191 return new DocumentTreeNodeImpl(doc, filter, leafFilter, sorter, pageProvider); 192 } 193 194 @Deprecated 195 public void changeExpandListener(CollapsibleSubTableToggleEvent event) { 196 FacesContext facesContext = FacesContext.getCurrentInstance(); 197 Map<String, Object> requestMap = facesContext.getExternalContext().getRequestMap(); 198 requestMap.put(NODE_SELECTED_MARKER, Boolean.TRUE); 199 } 200 201 public String getCurrentDocumentPath() { 202 if (currentDocumentPath == null) { 203 DocumentModel currentDoc = navigationContext.getCurrentDocument(); 204 if (currentDoc != null) { 205 currentDocumentPath = currentDoc.getPathAsString(); 206 } 207 } 208 return currentDocumentPath; 209 } 210 211 protected String getUserWorkspacePath() { 212 String currentDocumentPath = getCurrentDocumentPath(); 213 if (StringUtils.isBlank(currentPersonalWorkspacePath)) { 214 reset(); 215 return currentDocumentPath; 216 } 217 if (userWorkspacePath == null || !userWorkspacePath.contains(currentPersonalWorkspacePath)) { 218 // navigate to another personal workspace 219 reset(); 220 return documentManager.exists(new PathRef(currentPersonalWorkspacePath)) ? currentPersonalWorkspacePath 221 : findFarthestContainerPath(currentDocumentPath); 222 } 223 return userWorkspacePath; 224 } 225 226 protected String findFarthestContainerPath(String documentPath) { 227 Path containerPath = new Path(documentPath); 228 String result; 229 do { 230 result = containerPath.toString(); 231 containerPath = containerPath.removeLastSegments(1); 232 } while (!containerPath.isRoot() && documentManager.exists(new PathRef(containerPath.toString()))); 233 return result; 234 } 235 236 @Observer(value = { EventNames.USER_ALL_DOCUMENT_TYPES_SELECTION_CHANGED }, create = false) 237 @BypassInterceptors 238 public void resetCurrentDocumentData() { 239 currentDocumentPath = null; 240 if (checkIfTreeInvalidationNeeded()) { 241 trees.clear(); 242 return; 243 } 244 // reset tree in case an accessible parent is finally found this time 245 // for the new current document 246 for (List<DocumentTreeNode> tree : trees.values()) { 247 if (tree != null && tree.isEmpty()) { 248 tree = null; 249 } 250 } 251 } 252 253 protected boolean checkIfTreeInvalidationNeeded() { 254 // NXP-9813: this check may consume more resource, because called each 255 // time a document selection is changed but it guarantees a better 256 // detection if moving from one tree to another without using 257 // UserWorkspace actions from user menu, which raise appropriate events 258 DocumentModel currentDocument = (DocumentModel) Component.getInstance("currentDocument"); 259 if (currentDocument != null && showingGlobalRoot) { 260 return true; 261 } 262 if (currentDocument != null 263 && firstAccessibleParentPath != null 264 && currentDocument.getPathAsString() != null 265 && (!currentDocument.getPathAsString().contains(firstAccessibleParentPath) || (userWorkspacePath != null 266 && currentDocument.getPathAsString().contains(userWorkspacePath) && !firstAccessibleParentPath.contains(userWorkspacePath)))) { 267 return true; 268 } 269 return false; 270 } 271 272 @Observer(value = { EventNames.GO_HOME, EventNames.DOMAIN_SELECTION_CHANGED, EventNames.DOCUMENT_CHANGED, 273 EventNames.DOCUMENT_SECURITY_CHANGED, EventNames.DOCUMENT_CHILDREN_CHANGED }, create = false) 274 @BypassInterceptors 275 public void reset() { 276 trees.clear(); 277 resetCurrentDocumentData(); 278 } 279 280 @Observer(value = { EventNames.GO_PERSONAL_WORKSPACE }, create = true) 281 public void switchToUserWorkspace() { 282 userWorkspacePath = getCurrentDocumentPath(); 283 reset(); 284 } 285 286 @Observer(value = { EventNames.GO_HOME }, create = false) 287 @BypassInterceptors 288 public void switchToDocumentBase() { 289 } 290 291 public String forceTreeRefresh() throws IOException { 292 293 resetCurrentDocumentData(); 294 295 FacesContext context = FacesContext.getCurrentInstance(); 296 HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse(); 297 response.setContentType("application/xml; charset=UTF-8"); 298 response.getWriter().write("<response>OK</response>"); 299 context.responseComplete(); 300 301 return null; 302 } 303 304 /** 305 * @since 6.0 306 */ 307 public void toggleListener() { 308 FacesContext facesContext = FacesContext.getCurrentInstance(); 309 Map<String, Object> requestMap = facesContext.getExternalContext().getRequestMap(); 310 requestMap.put(NODE_SELECTED_MARKER, Boolean.TRUE); 311 } 312 313 /** 314 * @since 6.0 315 */ 316 public boolean isNodeExpandEvent() { 317 FacesContext facesContext = FacesContext.getCurrentInstance(); 318 if (facesContext != null) { 319 ExternalContext externalContext = facesContext.getExternalContext(); 320 if (externalContext != null) { 321 return Boolean.TRUE.equals(externalContext.getRequestMap().get(NODE_SELECTED_MARKER)); 322 } 323 } 324 return false; 325 } 326 327}