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