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