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