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:rcaraghin@nuxeo.com">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}