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