001/*
002 * (C) Copyright 2006-2007 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 *
019 * $Id$
020 */
021package org.nuxeo.ecm.webapp.tree;
022
023import static org.jboss.seam.ScopeType.CONVERSATION;
024import static org.jboss.seam.annotations.Install.FRAMEWORK;
025
026import java.io.IOException;
027import java.io.Serializable;
028import java.util.ArrayList;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032
033import javax.faces.context.ExternalContext;
034import javax.faces.context.FacesContext;
035import javax.servlet.http.HttpServletResponse;
036
037import org.apache.commons.lang.StringUtils;
038import org.apache.commons.logging.Log;
039import org.apache.commons.logging.LogFactory;
040import org.jboss.seam.Component;
041import org.jboss.seam.annotations.In;
042import org.jboss.seam.annotations.Install;
043import org.jboss.seam.annotations.Name;
044import org.jboss.seam.annotations.Observer;
045import org.jboss.seam.annotations.Scope;
046import org.jboss.seam.annotations.intercept.BypassInterceptors;
047import org.nuxeo.common.utils.Path;
048import org.nuxeo.ecm.core.api.CoreSession;
049import org.nuxeo.ecm.core.api.DocumentModel;
050import org.nuxeo.ecm.core.api.Filter;
051import org.nuxeo.ecm.core.api.PathRef;
052import org.nuxeo.ecm.core.api.Sorter;
053import org.nuxeo.ecm.platform.ui.web.api.NavigationContext;
054import org.nuxeo.ecm.webapp.helpers.EventNames;
055import org.nuxeo.runtime.api.Framework;
056import org.richfaces.event.CollapsibleSubTableToggleEvent;
057
058/**
059 * Manages the navigation tree.
060 *
061 * @author Razvan Caraghin
062 * @author Anahide Tchertchian
063 */
064@Scope(CONVERSATION)
065@Name("treeActions")
066@Install(precedence = FRAMEWORK)
067public class TreeActionsBean implements TreeActions, Serializable {
068
069    private static final long serialVersionUID = 1L;
070
071    private static final Log log = LogFactory.getLog(TreeActionsBean.class);
072
073    public static final String NODE_SELECTED_MARKER = TreeActionsBean.class.getName() + "_NODE_SELECTED_MARKER";
074
075    @In(create = true, required = false)
076    protected transient CoreSession documentManager;
077
078    @In(create = true)
079    protected transient NavigationContext navigationContext;
080
081    protected Map<String, List<DocumentTreeNode>> trees = new HashMap<String, List<DocumentTreeNode>>();
082
083    protected String currentDocumentPath;
084
085    @In(create = true, required = false)
086    protected Boolean isUserWorkspace;
087
088    @In(create = true, required = false)
089    protected String currentPersonalWorkspacePath;
090
091    protected String userWorkspacePath;
092
093    // cache the path of the tree root to check if invalidation are needed when
094    // bypassing interceptors
095    protected String firstAccessibleParentPath;
096
097    protected boolean showingGlobalRoot;
098
099    @In(create = true)
100    protected TreeInvalidatorBean treeInvalidator;
101
102    public List<DocumentTreeNode> getTreeRoots() {
103        return getTreeRoots(false);
104    }
105
106    public List<DocumentTreeNode> getTreeRoots(String treeName) {
107        return getTreeRoots(false, treeName);
108    }
109
110    protected List<DocumentTreeNode> getTreeRoots(boolean showRoot, String treeName) {
111        return getTreeRoots(showRoot, navigationContext.getCurrentDocument(), treeName);
112    }
113
114    protected List<DocumentTreeNode> getTreeRoots(boolean showRoot) {
115        return getTreeRoots(showRoot, navigationContext.getCurrentDocument(), DEFAULT_TREE_PLUGIN_NAME);
116    }
117
118    protected List<DocumentTreeNode> getTreeRoots(boolean showRoot, DocumentModel currentDocument)
119            {
120        return getTreeRoots(showRoot, currentDocument, DEFAULT_TREE_PLUGIN_NAME);
121    }
122
123    /**
124     * @since 5.4
125     */
126    protected List<DocumentTreeNode> getTreeRoots(boolean showRoot, DocumentModel currentDocument, String treeName)
127            {
128
129        if (treeInvalidator.needsInvalidation()) {
130            reset();
131            treeInvalidator.invalidationDone();
132        }
133        if (Boolean.TRUE.equals(isUserWorkspace)) {
134            userWorkspacePath = getUserWorkspacePath();
135        }
136        List<DocumentTreeNode> currentTree = trees.get(treeName);
137        if (currentTree == null) {
138            currentTree = new ArrayList<DocumentTreeNode>();
139            DocumentModel globalRoot = null;
140            DocumentModel firstAccessibleParent = null;
141            if (currentDocument != null) {
142
143                if (Boolean.TRUE.equals(isUserWorkspace)) {
144                    firstAccessibleParent = documentManager.getDocument(new PathRef(userWorkspacePath));
145                } else {
146
147                    List<DocumentModel> parents = documentManager.getParentDocuments(currentDocument.getRef());
148                    if (!parents.isEmpty()) {
149                        firstAccessibleParent = parents.get(0);
150                    } else if (!"Root".equals(currentDocument.getType()) && currentDocument.isFolder()) {
151                        // default on current doc
152                        firstAccessibleParent = currentDocument;
153                    } else {
154                        if (showRoot) {
155                            firstAccessibleParent = currentDocument;
156                        }
157                    }
158
159                }
160                if (showRoot
161                        && (firstAccessibleParent == null || !"/".equals(firstAccessibleParent.getPathAsString()))) {
162                    // also add the global root if we don't already show it and it's accessible
163                    if (documentManager.exists(new PathRef("/"))) {
164                        globalRoot = documentManager.getRootDocument();
165                    }
166                }
167            }
168            showingGlobalRoot = globalRoot != null;
169            if (showingGlobalRoot) {
170                DocumentTreeNode treeRoot = newDocumentTreeNode(globalRoot, treeName);
171                currentTree.add(treeRoot);
172                log.debug("Tree initialized with additional global root");
173            }
174            firstAccessibleParentPath = firstAccessibleParent == null ? null : firstAccessibleParent.getPathAsString();
175            if (firstAccessibleParent != null) {
176                DocumentTreeNode treeRoot = newDocumentTreeNode(firstAccessibleParent, treeName);
177                currentTree.add(treeRoot);
178                log.debug("Tree initialized with document: " + firstAccessibleParent.getId());
179            } else {
180                log.debug("Could not initialize the navigation tree: no parent" + " found for current document");
181            }
182            trees.put(treeName, currentTree);
183        }
184        return trees.get(treeName);
185    }
186
187    protected DocumentTreeNode newDocumentTreeNode(DocumentModel doc, String treeName) {
188        TreeManager treeManager = Framework.getService(TreeManager.class);
189        Filter filter = treeManager.getFilter(treeName);
190        Filter leafFilter = treeManager.getLeafFilter(treeName);
191        Sorter sorter = treeManager.getSorter(treeName);
192        String pageProvider = treeManager.getPageProviderName(treeName);
193        return new DocumentTreeNodeImpl(doc, filter, leafFilter, sorter, pageProvider);
194    }
195
196    @Deprecated
197    public void changeExpandListener(CollapsibleSubTableToggleEvent event) {
198        FacesContext facesContext = FacesContext.getCurrentInstance();
199        Map<String, Object> requestMap = facesContext.getExternalContext().getRequestMap();
200        requestMap.put(NODE_SELECTED_MARKER, Boolean.TRUE);
201    }
202
203    public String getCurrentDocumentPath() {
204        if (currentDocumentPath == null) {
205            DocumentModel currentDoc = navigationContext.getCurrentDocument();
206            if (currentDoc != null) {
207                currentDocumentPath = currentDoc.getPathAsString();
208            }
209        }
210        return currentDocumentPath;
211    }
212
213    protected String getUserWorkspacePath() {
214        String currentDocumentPath = getCurrentDocumentPath();
215        if (StringUtils.isBlank(currentPersonalWorkspacePath)) {
216            reset();
217            return currentDocumentPath;
218        }
219        if (userWorkspacePath == null || !userWorkspacePath.contains(currentPersonalWorkspacePath)) {
220            // navigate to another personal workspace
221            reset();
222            return documentManager.exists(new PathRef(currentPersonalWorkspacePath)) ? currentPersonalWorkspacePath
223                    : findFarthestContainerPath(currentDocumentPath);
224        }
225        return userWorkspacePath;
226    }
227
228    protected String findFarthestContainerPath(String documentPath) {
229        Path containerPath = new Path(documentPath);
230        String result;
231        do {
232            result = containerPath.toString();
233            containerPath = containerPath.removeLastSegments(1);
234        } while (!containerPath.isRoot() && documentManager.exists(new PathRef(containerPath.toString())));
235        return result;
236    }
237
238    @Observer(value = { EventNames.USER_ALL_DOCUMENT_TYPES_SELECTION_CHANGED }, create = false)
239    @BypassInterceptors
240    public void resetCurrentDocumentData() {
241        currentDocumentPath = null;
242        if (checkIfTreeInvalidationNeeded()) {
243            trees.clear();
244            return;
245        }
246        // reset tree in case an accessible parent is finally found this time
247        // for the new current document
248        for (List<DocumentTreeNode> tree : trees.values()) {
249            if (tree != null && tree.isEmpty()) {
250                tree = null;
251            }
252        }
253    }
254
255    protected boolean checkIfTreeInvalidationNeeded() {
256        // NXP-9813: this check may consume more resource, because called each
257        // time a document selection is changed but it guarantees a better
258        // detection if moving from one tree to another without using
259        // UserWorkspace actions from user menu, which raise appropriate events
260        DocumentModel currentDocument = (DocumentModel) Component.getInstance("currentDocument");
261        if (currentDocument != null && showingGlobalRoot) {
262            return true;
263        }
264        if (currentDocument != null
265                && firstAccessibleParentPath != null
266                && currentDocument.getPathAsString() != null
267                && (!currentDocument.getPathAsString().contains(firstAccessibleParentPath) || (userWorkspacePath != null
268                        && currentDocument.getPathAsString().contains(userWorkspacePath) && !firstAccessibleParentPath.contains(userWorkspacePath)))) {
269            return true;
270        }
271        return false;
272    }
273
274    @Observer(value = { EventNames.GO_HOME, EventNames.DOMAIN_SELECTION_CHANGED, EventNames.DOCUMENT_CHANGED,
275            EventNames.DOCUMENT_SECURITY_CHANGED, EventNames.DOCUMENT_CHILDREN_CHANGED }, create = false)
276    @BypassInterceptors
277    public void reset() {
278        trees.clear();
279        resetCurrentDocumentData();
280    }
281
282    @Observer(value = { EventNames.GO_PERSONAL_WORKSPACE }, create = true)
283    public void switchToUserWorkspace() {
284        userWorkspacePath = getCurrentDocumentPath();
285        reset();
286    }
287
288    @Observer(value = { EventNames.GO_HOME }, create = false)
289    @BypassInterceptors
290    public void switchToDocumentBase() {
291    }
292
293    public String forceTreeRefresh() throws IOException {
294
295        resetCurrentDocumentData();
296
297        FacesContext context = FacesContext.getCurrentInstance();
298        HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
299        response.setContentType("application/xml; charset=UTF-8");
300        response.getWriter().write("<response>OK</response>");
301        context.responseComplete();
302
303        return null;
304    }
305
306    /**
307     * @since 6.0
308     */
309    public void toggleListener() {
310        FacesContext facesContext = FacesContext.getCurrentInstance();
311        Map<String, Object> requestMap = facesContext.getExternalContext().getRequestMap();
312        requestMap.put(NODE_SELECTED_MARKER, Boolean.TRUE);
313    }
314
315    /**
316     * @since 6.0
317     */
318    public boolean isNodeExpandEvent() {
319        FacesContext facesContext = FacesContext.getCurrentInstance();
320        if (facesContext != null) {
321            ExternalContext externalContext = facesContext.getExternalContext();
322            if (externalContext != null) {
323                return Boolean.TRUE.equals(externalContext.getRequestMap().get(NODE_SELECTED_MARKER));
324            }
325        }
326        return false;
327    }
328
329}