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.DocumentModelList;
042import org.nuxeo.ecm.core.api.NuxeoException;
043import org.nuxeo.ecm.core.api.PropertyException;
044import org.nuxeo.ecm.core.api.impl.DocumentModelImpl;
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, new DocumentModelImpl("Folder"));
155        pathProcessing();
156        return config.getOutcome();
157    }
158
159    @SuppressWarnings("unchecked")
160    public boolean isSelected() {
161        if (config.hasContentViewSupport()) {
162            DocumentModel searchDoc = getContentViewSearchDocumentModel();
163            if (searchDoc != null) {
164                String fieldName = config.getFieldName();
165                String schemaName = config.getSchemaName();
166                if (config.isMultiselect()) {
167                    List<Object> values = (List<Object>) searchDoc.getProperty(schemaName, fieldName);
168                    return values.contains(path);
169                } else {
170                    return path.equals(searchDoc.getProperty(schemaName, fieldName));
171                }
172            } else {
173                log.error("Cannot check if node is selected: " + "search document model is null");
174            }
175        } else {
176            log.error(String.format("Cannot check if node is selected on tree '%s': no " + "content view available",
177                    identifier));
178        }
179        return false;
180    }
181
182    public int getChildCount() {
183        if (isLastLevel()) {
184            return 0;
185        }
186        return getChildrenEntries().size();
187    }
188
189    public List<DirectoryTreeNode> getChildren() {
190        if (children != null) {
191            // return last computed state
192            return children;
193        }
194        children = new ArrayList<DirectoryTreeNode>();
195        if (isLastLevel()) {
196            return children;
197        }
198        String schema = getDirectorySchema();
199        DocumentModelList results = getChildrenEntries();
200        FacesContext context = FacesContext.getCurrentInstance();
201        for (DocumentModel result : results) {
202            String childIdendifier = result.getId();
203            String childDescription = translate(context, (String) result.getProperty(schema, LABEL_FIELD_ID));
204            String childPath;
205            if ("".equals(path)) {
206                childPath = childIdendifier;
207            } else {
208                childPath = path + '/' + childIdendifier;
209            }
210            children.add(new DirectoryTreeNode(level + 1, config, childIdendifier, childDescription, childPath,
211                    getDirectoryService()));
212        }
213
214        // sort children
215        Comparator<? super DirectoryTreeNode> cmp = new FieldComparator();
216        Collections.sort(children, cmp);
217
218        return children;
219    }
220
221    private class FieldComparator implements Comparator<DirectoryTreeNode> {
222
223        @Override
224        public int compare(DirectoryTreeNode o1, DirectoryTreeNode o2) {
225            return ObjectUtils.compare(o1.getDescription(), o2.getDescription());
226        }
227    }
228
229    protected static String translate(FacesContext context, String label) {
230        String bundleName = context.getApplication().getMessageBundle();
231        Locale locale = context.getViewRoot().getLocale();
232        label = I18NUtils.getMessageString(bundleName, label, null, locale);
233        return label;
234    }
235
236    protected DocumentModelList getChildrenEntries() {
237        if (childrenEntries != null) {
238            // memorized directory lookup since directory content is not
239            // suppose to change
240            // XXX: use the cache manager instead of field caching strategy
241            return childrenEntries;
242        }
243        try (Session session = getDirectorySession()) {
244            if (level == 0) {
245                String schemaName = getDirectorySchema();
246                SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
247                Schema schema = schemaManager.getSchema(schemaName);
248                if (schema.hasField(PARENT_FIELD_ID)) {
249                    // filter on empty parent
250                    Map<String, Serializable> filter = new HashMap<String, Serializable>();
251                    filter.put(PARENT_FIELD_ID, "");
252                    childrenEntries = session.query(filter);
253                } else {
254                    childrenEntries = session.getEntries();
255                }
256            } else {
257                Map<String, Serializable> filter = new HashMap<String, Serializable>();
258                String[] bitsOfPath = path.split("/");
259                filter.put(PARENT_FIELD_ID, bitsOfPath[level - 1]);
260                childrenEntries = session.query(filter);
261            }
262            return childrenEntries;
263        }
264    }
265
266    public String getDescription() {
267        if (level == 0) {
268            return translate(FacesContext.getCurrentInstance(), description);
269        }
270        return description;
271    }
272
273    public String getIdentifier() {
274        return identifier;
275    }
276
277    public String getPath() {
278        return path;
279    }
280
281    public String getType() {
282        return type;
283    }
284
285    public boolean isLeaf() {
286        return leaf || isLastLevel() || getChildCount() == 0;
287    }
288
289    public void setDescription(String description) {
290        this.description = description;
291    }
292
293    public void setIdentifier(String identifier) {
294        this.identifier = identifier;
295    }
296
297    public void setLeaf(boolean leaf) {
298        this.leaf = leaf;
299    }
300
301    public void setType(String type) {
302        this.type = type;
303    }
304
305    protected DirectoryService getDirectoryService() {
306        if (directoryService == null) {
307            directoryService = DirectoryHelper.getDirectoryService();
308        }
309        return directoryService;
310    }
311
312    protected String getDirectoryName() {
313        String name = config.getDirectories()[level];
314        if (name == null) {
315            throw new NuxeoException("could not find directory name for level=" + level);
316        }
317        return name;
318    }
319
320    protected String getDirectorySchema() {
321        return getDirectoryService().getDirectorySchema(getDirectoryName());
322    }
323
324    protected Session getDirectorySession() {
325        return getDirectoryService().open(getDirectoryName());
326    }
327
328    protected void lookupContentView() {
329        if (contentView != null) {
330            return;
331        }
332        SeamContextHelper seamContextHelper = new SeamContextHelper();
333        ContentViewActions cva = (ContentViewActions) seamContextHelper.get("contentViewActions");
334        contentView = cva.getContentView(config.getContentView());
335        if (contentView == null) {
336            throw new NuxeoException("no content view registered as " + config.getContentView());
337        }
338    }
339
340    protected DocumentModel getContentViewSearchDocumentModel() {
341        lookupContentView();
342        if (contentView != null) {
343            return contentView.getSearchDocumentModel();
344        }
345        return null;
346    }
347
348    protected boolean isLastLevel() {
349        return config.getDirectories().length == level;
350    }
351
352    public void pathProcessing() throws DirectoryException {
353        if (config.isMultiselect()) {
354            // no breadcrumbs management with multiselect
355            return;
356        }
357        String aPath = null;
358        if (config.hasContentViewSupport()) {
359            DocumentModel searchDoc = getContentViewSearchDocumentModel();
360            if (searchDoc != null) {
361                aPath = (String) searchDoc.getProperty(config.getSchemaName(), config.getFieldName());
362            } else {
363                log.error("Cannot perform path preprocessing: " + "search document model is null");
364            }
365        }
366        if (aPath != null && aPath != "") {
367            String[] bitsOfPath = aPath.split("/");
368            String myPath = "";
369            String property = "";
370            for (int b = 0; b < bitsOfPath.length; b++) {
371                String dirName = config.getDirectories()[b];
372                if (dirName == null) {
373                    throw new DirectoryException("Could not find directory name for key=" + b);
374                }
375                try (Session session = getDirectoryService().open(dirName)) {
376                    DocumentModel docMod = session.getEntry(bitsOfPath[b]);
377                    try {
378                        // take first schema: directory entries only have one
379                        final String schemaName = docMod.getSchemas()[0];
380                        property = (String) docMod.getProperty(schemaName, LABEL_FIELD_ID);
381                    } catch (PropertyException e) {
382                        throw new DirectoryException(e);
383                    }
384                    myPath = myPath + property + '/';
385                }
386            }
387            Events.instance().raiseEvent("PATH_PROCESSED", myPath);
388        } else {
389            Events.instance().raiseEvent("PATH_PROCESSED", "");
390        }
391    }
392
393    /**
394     * @deprecated since 6.0, use {@link #isOpen()} instead
395     */
396    @Deprecated
397    public boolean isOpened() {
398        return isOpen();
399    }
400
401    public boolean isOpen() {
402        if (open == null) {
403            final TreeActions treeActionBean = (TreeActionsBean) Component.getInstance("treeActions");
404            if (!treeActionBean.isNodeExpandEvent()) {
405                if (!config.isMultiselect() && config.hasContentViewSupport()) {
406                    DocumentModel searchDoc = getContentViewSearchDocumentModel();
407                    if (searchDoc != null) {
408                        String fieldName = config.getFieldName();
409                        String schemaName = config.getSchemaName();
410                        Object value = searchDoc.getProperty(schemaName, fieldName);
411                        if (value instanceof String) {
412                            open = Boolean.valueOf(((String) value).startsWith(path));
413                        }
414                    } else {
415                        log.error("Cannot check if node is opened: " + "search document model is null");
416                    }
417                } else {
418                    log.error(String.format("Cannot check if node is opened on tree '%s': no "
419                            + "content view available", identifier));
420                }
421            }
422        }
423        return Boolean.TRUE.equals(open);
424    }
425
426    public void setOpen(boolean open) {
427        this.open = Boolean.valueOf(open);
428    }
429
430}