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