001/*
002 * (C) Copyright 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: DirectoryTreeNode.java 29611 2008-01-24 16:51:03Z gracinet $
018 */
019package org.nuxeo.ecm.webapp.directory;
020
021import java.io.Serializable;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.Comparator;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Locale;
028import java.util.Map;
029
030import javax.faces.context.FacesContext;
031
032import org.apache.commons.lang.ObjectUtils;
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.jboss.seam.Component;
036import org.jboss.seam.core.Events;
037import org.nuxeo.common.utils.i18n.I18NUtils;
038import org.nuxeo.ecm.core.api.DocumentModel;
039import org.nuxeo.ecm.core.api.DocumentModelList;
040import org.nuxeo.ecm.core.api.NuxeoException;
041import org.nuxeo.ecm.core.api.PropertyException;
042import org.nuxeo.ecm.core.api.impl.DocumentModelImpl;
043import org.nuxeo.ecm.core.schema.SchemaManager;
044import org.nuxeo.ecm.core.schema.types.Schema;
045import org.nuxeo.ecm.directory.DirectoryException;
046import org.nuxeo.ecm.directory.Session;
047import org.nuxeo.ecm.directory.api.DirectoryService;
048import org.nuxeo.ecm.platform.contentview.jsf.ContentView;
049import org.nuxeo.ecm.platform.contentview.seam.ContentViewActions;
050import org.nuxeo.ecm.platform.ui.web.directory.DirectoryHelper;
051import org.nuxeo.ecm.platform.ui.web.util.SeamContextHelper;
052import org.nuxeo.ecm.webapp.helpers.EventNames;
053import org.nuxeo.ecm.webapp.tree.TreeActions;
054import org.nuxeo.ecm.webapp.tree.TreeActionsBean;
055import org.nuxeo.runtime.api.Framework;
056
057/**
058 * Register directory tree configurations to make them available to the DirectoryTreeManagerBean to build
059 * DirectoryTreeNode instances.
060 *
061 * @author <a href="mailto:ogrisel@nuxeo.com">Olivier Grisel</a>
062 */
063public class DirectoryTreeNode {
064
065    private static final Log log = LogFactory.getLog(DirectoryTreeNode.class);
066
067    public static final String PARENT_FIELD_ID = "parent";
068
069    private static final String LABEL_FIELD_ID = "label";
070
071    protected final String path;
072
073    protected final int level;
074
075    protected Boolean open = null;
076
077    protected final DirectoryTreeDescriptor config;
078
079    protected String identifier;
080
081    protected String description;
082
083    protected boolean leaf = false;
084
085    protected String type = "defaultDirectoryTreeNode";
086
087    protected DirectoryService directoryService;
088
089    protected ContentView contentView;
090
091    protected DocumentModelList childrenEntries;
092
093    protected List<DirectoryTreeNode> children;
094
095    public DirectoryTreeNode(int level, DirectoryTreeDescriptor config, String identifier, String description,
096            String path, DirectoryService directoryService) {
097        this.level = level;
098        this.config = config;
099        this.identifier = identifier;
100        this.description = description;
101        this.path = path;
102        this.directoryService = directoryService;
103    }
104
105    protected List<String> processSelectedValuesOnMultiSelect(String value, List<String> values) {
106        if (values.contains(value)) {
107            values.remove(value);
108        } else {
109            // unselect all previous selection that are either more
110            // generic or more specific
111            List<String> valuesToRemove = new ArrayList<String>();
112            String valueSlash = value + "/";
113            for (String existingSelection : values) {
114                String existingSelectionSlash = existingSelection + "/";
115                if (existingSelectionSlash.startsWith(valueSlash) || valueSlash.startsWith(existingSelectionSlash)) {
116                    valuesToRemove.add(existingSelection);
117                }
118            }
119            values.removeAll(valuesToRemove);
120
121            // add the new selection
122            values.add(value);
123        }
124        return values;
125    }
126
127    @SuppressWarnings("unchecked")
128    public String selectNode() {
129        if (config.hasContentViewSupport()) {
130            DocumentModel searchDoc = getContentViewSearchDocumentModel();
131            if (searchDoc != null) {
132                String fieldName = config.getFieldName();
133                String schemaName = config.getSchemaName();
134                if (config.isMultiselect()) {
135                    List<String> values = (List<String>) searchDoc.getProperty(schemaName, fieldName);
136                    values = processSelectedValuesOnMultiSelect(path, values);
137                    searchDoc.setProperty(schemaName, fieldName, values);
138                } else {
139                    searchDoc.setProperty(schemaName, fieldName, path);
140                }
141                if (contentView != null) {
142                    contentView.refreshPageProvider();
143                }
144            } else {
145                log.error("Cannot select node: search document model is null");
146            }
147        } else {
148            log.error(String.format("Cannot select node on tree '%s': no content view available", identifier));
149        }
150        // raise this event in order to reset the documents lists from
151        // 'conversationDocumentsListsManager'
152        Events.instance().raiseEvent(EventNames.FOLDERISHDOCUMENT_SELECTION_CHANGED, new DocumentModelImpl("Folder"));
153        pathProcessing();
154        return config.getOutcome();
155    }
156
157    @SuppressWarnings("unchecked")
158    public boolean isSelected() {
159        if (config.hasContentViewSupport()) {
160            DocumentModel searchDoc = getContentViewSearchDocumentModel();
161            if (searchDoc != null) {
162                String fieldName = config.getFieldName();
163                String schemaName = config.getSchemaName();
164                if (config.isMultiselect()) {
165                    List<Object> values = (List<Object>) searchDoc.getProperty(schemaName, fieldName);
166                    return values.contains(path);
167                } else {
168                    return path.equals(searchDoc.getProperty(schemaName, fieldName));
169                }
170            } else {
171                log.error("Cannot check if node is selected: " + "search document model is null");
172            }
173        } else {
174            log.error(String.format("Cannot check if node is selected on tree '%s': no " + "content view available",
175                    identifier));
176        }
177        return false;
178    }
179
180    public int getChildCount() {
181        if (isLastLevel()) {
182            return 0;
183        }
184        return getChildrenEntries().size();
185    }
186
187    public List<DirectoryTreeNode> getChildren() {
188        if (children != null) {
189            // return last computed state
190            return children;
191        }
192        children = new ArrayList<DirectoryTreeNode>();
193        if (isLastLevel()) {
194            return children;
195        }
196        String schema = getDirectorySchema();
197        DocumentModelList results = getChildrenEntries();
198        FacesContext context = FacesContext.getCurrentInstance();
199        for (DocumentModel result : results) {
200            String childIdendifier = result.getId();
201            String childDescription = translate(context, (String) result.getProperty(schema, LABEL_FIELD_ID));
202            String childPath;
203            if ("".equals(path)) {
204                childPath = childIdendifier;
205            } else {
206                childPath = path + '/' + childIdendifier;
207            }
208            children.add(new DirectoryTreeNode(level + 1, config, childIdendifier, childDescription, childPath,
209                    getDirectoryService()));
210        }
211
212        // sort children
213        Comparator<? super DirectoryTreeNode> cmp = new FieldComparator();
214        Collections.sort(children, cmp);
215
216        return children;
217    }
218
219    private class FieldComparator implements Comparator<DirectoryTreeNode> {
220
221        @Override
222        public int compare(DirectoryTreeNode o1, DirectoryTreeNode o2) {
223            return ObjectUtils.compare(o1.getDescription(), o2.getDescription());
224        }
225    }
226
227    protected static String translate(FacesContext context, String label) {
228        String bundleName = context.getApplication().getMessageBundle();
229        Locale locale = context.getViewRoot().getLocale();
230        label = I18NUtils.getMessageString(bundleName, label, null, locale);
231        return label;
232    }
233
234    protected DocumentModelList getChildrenEntries() {
235        if (childrenEntries != null) {
236            // memorized directory lookup since directory content is not
237            // suppose to change
238            // XXX: use the cache manager instead of field caching strategy
239            return childrenEntries;
240        }
241        try (Session session = getDirectorySession()) {
242            if (level == 0) {
243                String schemaName = getDirectorySchema();
244                SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
245                Schema schema = schemaManager.getSchema(schemaName);
246                if (schema.hasField(PARENT_FIELD_ID)) {
247                    // filter on empty parent
248                    Map<String, Serializable> filter = new HashMap<String, Serializable>();
249                    filter.put(PARENT_FIELD_ID, "");
250                    childrenEntries = session.query(filter);
251                } else {
252                    childrenEntries = session.getEntries();
253                }
254            } else {
255                Map<String, Serializable> filter = new HashMap<String, Serializable>();
256                String[] bitsOfPath = path.split("/");
257                filter.put(PARENT_FIELD_ID, bitsOfPath[level - 1]);
258                childrenEntries = session.query(filter);
259            }
260            return childrenEntries;
261        }
262    }
263
264    public String getDescription() {
265        if (level == 0) {
266            return translate(FacesContext.getCurrentInstance(), description);
267        }
268        return description;
269    }
270
271    public String getIdentifier() {
272        return identifier;
273    }
274
275    public String getPath() {
276        return path;
277    }
278
279    public String getType() {
280        return type;
281    }
282
283    public boolean isLeaf() {
284        return leaf || isLastLevel() || getChildCount() == 0;
285    }
286
287    public void setDescription(String description) {
288        this.description = description;
289    }
290
291    public void setIdentifier(String identifier) {
292        this.identifier = identifier;
293    }
294
295    public void setLeaf(boolean leaf) {
296        this.leaf = leaf;
297    }
298
299    public void setType(String type) {
300        this.type = type;
301    }
302
303    protected DirectoryService getDirectoryService() {
304        if (directoryService == null) {
305            directoryService = DirectoryHelper.getDirectoryService();
306        }
307        return directoryService;
308    }
309
310    protected String getDirectoryName() {
311        String name = config.getDirectories()[level];
312        if (name == null) {
313            throw new NuxeoException("could not find directory name for level=" + level);
314        }
315        return name;
316    }
317
318    protected String getDirectorySchema() {
319        return getDirectoryService().getDirectorySchema(getDirectoryName());
320    }
321
322    protected Session getDirectorySession() {
323        return getDirectoryService().open(getDirectoryName());
324    }
325
326    protected void lookupContentView() {
327        if (contentView != null) {
328            return;
329        }
330        SeamContextHelper seamContextHelper = new SeamContextHelper();
331        ContentViewActions cva = (ContentViewActions) seamContextHelper.get("contentViewActions");
332        contentView = cva.getContentView(config.getContentView());
333        if (contentView == null) {
334            throw new NuxeoException("no content view registered as " + config.getContentView());
335        }
336    }
337
338    protected DocumentModel getContentViewSearchDocumentModel() {
339        lookupContentView();
340        if (contentView != null) {
341            return contentView.getSearchDocumentModel();
342        }
343        return null;
344    }
345
346    protected boolean isLastLevel() {
347        return config.getDirectories().length == level;
348    }
349
350    public void pathProcessing() throws DirectoryException {
351        if (config.isMultiselect()) {
352            // no breadcrumbs management with multiselect
353            return;
354        }
355        String aPath = null;
356        if (config.hasContentViewSupport()) {
357            DocumentModel searchDoc = getContentViewSearchDocumentModel();
358            if (searchDoc != null) {
359                aPath = (String) searchDoc.getProperty(config.getSchemaName(), config.getFieldName());
360            } else {
361                log.error("Cannot perform path preprocessing: " + "search document model is null");
362            }
363        }
364        if (aPath != null && aPath != "") {
365            String[] bitsOfPath = aPath.split("/");
366            String myPath = "";
367            String property = "";
368            for (int b = 0; b < bitsOfPath.length; b++) {
369                String dirName = config.getDirectories()[b];
370                if (dirName == null) {
371                    throw new DirectoryException("Could not find directory name for key=" + b);
372                }
373                try (Session session = getDirectoryService().open(dirName)) {
374                    DocumentModel docMod = session.getEntry(bitsOfPath[b]);
375                    try {
376                        // take first schema: directory entries only have one
377                        final String schemaName = docMod.getSchemas()[0];
378                        property = (String) docMod.getProperty(schemaName, LABEL_FIELD_ID);
379                    } catch (PropertyException e) {
380                        throw new DirectoryException(e);
381                    }
382                    myPath = myPath + property + '/';
383                }
384            }
385            Events.instance().raiseEvent("PATH_PROCESSED", myPath);
386        } else {
387            Events.instance().raiseEvent("PATH_PROCESSED", "");
388        }
389    }
390
391    /**
392     * @deprecated since 6.0, use {@link #isOpen()} instead
393     */
394    @Deprecated
395    public boolean isOpened() {
396        return isOpen();
397    }
398
399    public boolean isOpen() {
400        if (open == null) {
401            final TreeActions treeActionBean = (TreeActionsBean) Component.getInstance("treeActions");
402            if (!treeActionBean.isNodeExpandEvent()) {
403                if (!config.isMultiselect() && config.hasContentViewSupport()) {
404                    DocumentModel searchDoc = getContentViewSearchDocumentModel();
405                    if (searchDoc != null) {
406                        String fieldName = config.getFieldName();
407                        String schemaName = config.getSchemaName();
408                        Object value = searchDoc.getProperty(schemaName, fieldName);
409                        if (value instanceof String) {
410                            open = Boolean.valueOf(((String) value).startsWith(path));
411                        }
412                    } else {
413                        log.error("Cannot check if node is opened: " + "search document model is null");
414                    }
415                } else {
416                    log.error(String.format("Cannot check if node is opened on tree '%s': no "
417                            + "content view available", identifier));
418                }
419            }
420        }
421        return Boolean.TRUE.equals(open);
422    }
423
424    public void setOpen(boolean open) {
425        this.open = Boolean.valueOf(open);
426    }
427
428}