001/* 002 * (C) Copyright 2006-2012 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 */ 019package org.nuxeo.ecm.webapp.navigation; 020 021import static org.jboss.seam.ScopeType.CONVERSATION; 022import static org.jboss.seam.ScopeType.EVENT; 023import static org.jboss.seam.annotations.Install.FRAMEWORK; 024 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.FacesContext; 032 033import org.jboss.seam.annotations.Factory; 034import org.jboss.seam.annotations.In; 035import org.jboss.seam.annotations.Install; 036import org.jboss.seam.annotations.Name; 037import org.jboss.seam.annotations.Observer; 038import org.jboss.seam.annotations.Scope; 039import org.jboss.seam.navigation.Pages; 040import org.nuxeo.ecm.core.api.CoreSession; 041import org.nuxeo.ecm.core.api.DocumentModel; 042import org.nuxeo.ecm.core.api.security.SecurityConstants; 043import org.nuxeo.ecm.platform.query.api.PageProvider; 044import org.nuxeo.ecm.platform.query.api.PageProviderService; 045import org.nuxeo.ecm.platform.ui.web.api.NavigationContext; 046import org.nuxeo.ecm.platform.ui.web.pathelements.ArchivedVersionsPathElement; 047import org.nuxeo.ecm.platform.ui.web.pathelements.DocumentPathElement; 048import org.nuxeo.ecm.platform.ui.web.pathelements.PathElement; 049import org.nuxeo.ecm.platform.ui.web.pathelements.TextPathElement; 050import org.nuxeo.ecm.platform.ui.web.pathelements.VersionDocumentPathElement; 051import org.nuxeo.ecm.webapp.helpers.EventNames; 052import org.nuxeo.ecm.webapp.helpers.ResourcesAccessor; 053import org.nuxeo.ecm.webapp.helpers.StartupHelper; 054import org.nuxeo.runtime.api.Framework; 055 056/** 057 * The new approach: keep all selected documents into a list. Add new document to the list each time a new document is 058 * selected, after rebuilding the path. 059 * <p> 060 * Algorithm for rebuilding the path: 061 * <p> 062 * d1 -> d2 -> d3 -> d4 063 * <p> 064 * A new document is selected, which is a child of d2, named d2.5. We need to add d2.5 to the list after all unneeded 065 * documents have been removed to the list. In the end the list should look like this: d1 -> d2 -> d2.5. We need to 066 * remove all the documents in the list after d2, and add d2.5 to the list. TODO: fix bug when selecting an item located 067 * on a different branch than the current one so that its parent is not found in the current branch 068 * 069 * @author <a href="mailto:[email protected]">Razvan Caraghin</a> 070 */ 071@Name("breadcrumbActions") 072@Scope(CONVERSATION) 073@Install(precedence = FRAMEWORK) 074public class BreadcrumbActionsBean implements BreadcrumbActions, Serializable { 075 076 private static final long serialVersionUID = 1L; 077 078 public static final String BREADCRUMB_USER_DOMAINS_PROVIDER = "breadcrumb_user_domains"; 079 080 @In(create = true) 081 protected NavigationContext navigationContext; 082 083 @In(create = true, required = false) 084 protected CoreSession documentManager; 085 086 @In(create = true) 087 protected ResourcesAccessor resourcesAccessor; 088 089 protected List<DocumentModel> userDomains = null; 090 091 protected boolean isPathShrinked = false; 092 093 /** View id description prefix for message label (followed by "="). */ 094 protected static final String BREADCRUMB_PREFIX = "breadcrumb"; 095 096 /** 097 * Minimum path segments that must be displayed without shrinking. 098 */ 099 protected int getMinPathSegmentsLen() { 100 return 4; 101 } 102 103 /** 104 * Maximum length path that can be displayed without shrinking. 105 */ 106 protected int getMaxPathCharLen() { 107 return 80; 108 } 109 110 public String getPathEllipsis() { 111 return "…"; 112 } 113 114 public boolean isGoToParentButtonShown() { 115 return this.isPathShrinked 116 && !FacesContext.getCurrentInstance() 117 .getViewRoot() 118 .getViewId() 119 .equals("/" + StartupHelper.SERVERS_VIEW + ".xhtml"); 120 } 121 122 protected String getViewDomainsOutcome() { 123 return StartupHelper.DOMAINS_VIEW; 124 } 125 126 @Override 127 public String navigateToParent() { 128 List<PathElement> documentsFormingPath = getBackendPath(); 129 int nbDocInList = documentsFormingPath.size(); 130 // if there is the case, remove the starting 131 if (nbDocInList > 0 && documentsFormingPath.get(0).getName().equals(getPathEllipsis())) { 132 documentsFormingPath.remove(0); 133 } 134 135 nbDocInList = documentsFormingPath.size(); 136 137 if (nbDocInList == 0) { 138 return StartupHelper.SERVERS_VIEW; 139 } 140 141 String outcome; 142 if (nbDocInList > 1) { 143 PathElement parentPathElement = documentsFormingPath.get(nbDocInList - 2); 144 outcome = navigateToPathElement(parentPathElement); 145 } else { 146 PathElement pathElement = documentsFormingPath.get(0); 147 if (pathElement instanceof TextPathElement) { 148 DocumentModel currentDocument = navigationContext.getCurrentDocument(); 149 if (currentDocument == null) { 150 return StartupHelper.SERVERS_VIEW; 151 } else { 152 return navigationContext.navigateToDocument(currentDocument); 153 } 154 } 155 156 DocumentPathElement currentPathELement = (DocumentPathElement) pathElement; 157 DocumentModel doc = currentPathELement.getDocumentModel(); 158 159 if (documentManager.hasPermission(doc.getParentRef(), SecurityConstants.READ)) { 160 outcome = navigationContext.navigateToRef(doc.getParentRef()); 161 } else { 162 outcome = navigateToPathElement(currentPathELement); 163 } 164 if (navigationContext.getCurrentDocument().getType().equals("CoreRoot")) { 165 outcome = getViewDomainsOutcome(); 166 } 167 } 168 return outcome; 169 } 170 171 protected String navigateToPathElement(PathElement pathElement) { 172 // the bijection is not dynamic, i.e. the variables are injected 173 // before the action listener code is called. 174 String elementType = pathElement.getType(); 175 DocumentModel currentDoc; 176 if (elementType == DocumentPathElement.TYPE) { 177 DocumentPathElement docPathElement = (DocumentPathElement) pathElement; 178 currentDoc = docPathElement.getDocumentModel(); 179 return navigationContext.navigateToDocument(currentDoc); 180 } else if (elementType == ArchivedVersionsPathElement.TYPE) { 181 ArchivedVersionsPathElement docPathElement = (ArchivedVersionsPathElement) pathElement; 182 currentDoc = docPathElement.getDocumentModel(); 183 return navigationContext.navigateToDocument(currentDoc, "TAB_CONTENT_HISTORY"); 184 } else if (elementType == VersionDocumentPathElement.TYPE) { 185 VersionDocumentPathElement element = (VersionDocumentPathElement) pathElement; 186 currentDoc = element.getDocumentModel(); 187 return navigationContext.navigateToDocument(currentDoc); 188 } 189 return null; 190 } 191 192 /** 193 * Computes the current path by making calls to backend. TODO: need to change to compute the path from the seam 194 * context state. 195 * <p> 196 * GR: removed the Factory annotation because it made the method be called too early in case of processing that 197 * involves changing the current document. Multiple invocation of this method is anyway very cheap. 198 * 199 * @return 200 */ 201 @Override 202 @Factory(value = "backendPath", scope = EVENT) 203 public List<PathElement> getBackendPath() { 204 String viewId = FacesContext.getCurrentInstance().getViewRoot().getViewId(); 205 String viewIdLabel = Pages.instance().getPage(viewId).getDescription(); 206 if (viewIdLabel != null && viewIdLabel.startsWith(BREADCRUMB_PREFIX)) { 207 return makeBackendPathFromLabel(viewIdLabel.substring(BREADCRUMB_PREFIX.length() + 1)); 208 } else { 209 return shrinkPathIfNeeded(navigationContext.getCurrentPathList()); 210 } 211 } 212 213 @Factory(value = "isNavigationBreadcrumb", scope = EVENT) 214 public boolean isNavigationBreadcrumb() { 215 String viewId = FacesContext.getCurrentInstance().getViewRoot().getViewId(); 216 String viewIdLabel = Pages.instance().getPage(viewId).getDescription(); 217 return !((viewIdLabel != null) && viewIdLabel.startsWith(BREADCRUMB_PREFIX)); 218 } 219 220 protected List<PathElement> shrinkPathIfNeeded(List<PathElement> paths) { 221 222 if (paths == null || paths.size() <= getMinPathSegmentsLen()) { 223 this.isPathShrinked = false; 224 return paths; 225 } 226 227 StringBuffer sb = new StringBuffer(); 228 for (PathElement pe : paths) { 229 sb.append(pe.getName()); 230 } 231 String completePath = sb.toString(); 232 233 if (completePath.length() <= getMaxPathCharLen()) { 234 this.isPathShrinked = false; 235 return paths; 236 } 237 238 // shrink path 239 sb = new StringBuffer(); 240 List<PathElement> shrinkedPath = new ArrayList<PathElement>(); 241 for (int i = paths.size() - 1; i >= 0; i--) { 242 PathElement pe = paths.get(i); 243 sb.append(pe.getName()); 244 if (sb.length() < getMaxPathCharLen()) { 245 shrinkedPath.add(0, pe); 246 } else { 247 break; 248 } 249 } 250 // be sure we have at least one item in the breadcrumb otherwise the upnavigation will fail 251 if (shrinkedPath.size() == 0) { 252 // this means the current document has a title longer than MAX_PATH_CHAR_LEN ! 253 shrinkedPath.add(0, paths.get(paths.size() - 1)); 254 } 255 this.isPathShrinked = true; 256 return shrinkedPath; 257 } 258 259 protected List<PathElement> makeBackendPathFromLabel(String label) { 260 List<PathElement> pathElements = new ArrayList<PathElement>(); 261 label = resourcesAccessor.getMessages().get(label); 262 PathElement pathLabel = new TextPathElement(label); 263 // add the label of the viewId to the path 264 pathElements.add(pathLabel); 265 return pathElements; 266 } 267 268 @SuppressWarnings("unchecked") 269 public List<DocumentModel> getUserDomains() { 270 if (userDomains == null) { 271 PageProviderService pageProviderService = Framework.getService(PageProviderService.class); 272 Map<String, Serializable> properties = new HashMap<>(); 273 properties.put("coreSession", (Serializable) documentManager); 274 userDomains = ((PageProvider<DocumentModel>) pageProviderService.getPageProvider( 275 BREADCRUMB_USER_DOMAINS_PROVIDER, null, null, null, properties)).getCurrentPage(); 276 } 277 return userDomains; 278 } 279 280 public boolean isUserDomain(DocumentModel doc) { 281 List<DocumentModel> userDomains = getUserDomains(); 282 for (DocumentModel userDomain : userDomains) { 283 if (doc.getRef().equals(userDomain.getRef())) { 284 return true; 285 } 286 } 287 return false; 288 } 289 290 @Observer({ EventNames.LOCATION_SELECTION_CHANGED, EventNames.DOCUMENT_CHILDREN_CHANGED, 291 EventNames.DOCUMENT_CHANGED }) 292 public void resetUserDomains() { 293 userDomains = null; 294 } 295}