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}