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