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}