001/*
002 * (C) Copyright 2010 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 */
018package org.nuxeo.ecm.webapp.directory;
019
020import java.io.Serializable;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.Comparator;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Locale;
027import java.util.Map;
028
029import javax.faces.context.FacesContext;
030
031import org.apache.commons.lang.ObjectUtils;
032import org.apache.commons.lang.StringUtils;
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.nuxeo.ecm.core.api.DocumentModel;
036import org.nuxeo.ecm.core.api.DocumentModelList;
037import org.nuxeo.ecm.core.api.PropertyException;
038import org.nuxeo.ecm.core.schema.SchemaManager;
039import org.nuxeo.ecm.core.schema.types.Schema;
040import org.nuxeo.ecm.directory.DirectoryException;
041import org.nuxeo.ecm.directory.Session;
042import org.nuxeo.ecm.directory.api.DirectoryService;
043import org.nuxeo.ecm.platform.ui.web.directory.DirectoryHelper;
044import org.nuxeo.runtime.api.Framework;
045
046/**
047 * A vocabulary tree node based on l10nvocabulary or l10nxvocabulary directory. These schemas store translations in
048 * columns of the form label_xx_XX or label_xx. The label of a node is retrieved from column label_xx_XX (where xx_XX is
049 * the current locale name) if it exists, from column label_xx (where xx is the current locale language) else. If this
050 * one doesn't exist either, the english label (from label_en) is used.
051 *
052 * @since 5.5
053 * @author <a href="mailto:qlamerand@nuxeo.com">Quentin Lamerand</a>
054 */
055public class VocabularyTreeNode {
056
057    private static final Log log = LogFactory.getLog(VocabularyTreeNode.class);
058
059    public static final String PARENT_FIELD_ID = "parent";
060
061    public static final String LABEL_FIELD_PREFIX = "label_";
062
063    public static final String DEFAULT_LANGUAGE = "en";
064
065    public static final String OBSOLETE_FIELD = "obsolete";
066
067    protected final String path;
068
069    protected final int level;
070
071    protected String id;
072
073    protected String label;
074
075    protected DirectoryService directoryService;
076
077    protected List<VocabularyTreeNode> children;
078
079    protected String vocabularyName;
080
081    protected DocumentModelList childrenEntries;
082
083    protected boolean displayObsoleteEntries;
084
085    protected String orderingField;
086
087    protected Comparable orderingValue;
088
089    protected char keySeparator;
090
091    public VocabularyTreeNode(int level, String id, String description, String path, String vocabularyName,
092            DirectoryService directoryService) {
093        this(level, id, description, path, vocabularyName, directoryService, false, '/', null);
094    }
095
096    public VocabularyTreeNode(int level, String id, String description, String path, String vocabularyName,
097            DirectoryService directoryService, boolean displayObsoleteEntries, char keySeparator, String orderingField) {
098        this(level, id, description, path, vocabularyName, directoryService, displayObsoleteEntries, keySeparator,
099                orderingField, null);
100    }
101
102    public VocabularyTreeNode(int level, String id, String description, String path, String vocabularyName,
103            DirectoryService directoryService, boolean displayObsoleteEntries, char keySeparator, String orderingField,
104            Comparable orderingValue) {
105        this.level = level;
106        this.id = id;
107        this.label = description;
108        this.path = path;
109        this.vocabularyName = vocabularyName;
110        this.directoryService = directoryService;
111        this.displayObsoleteEntries = displayObsoleteEntries;
112        this.keySeparator = keySeparator;
113        this.orderingField = orderingField;
114        this.orderingValue = orderingValue;
115    }
116
117    public List<VocabularyTreeNode> getChildren() {
118        if (children != null) {
119            return children;
120        }
121        children = new ArrayList<VocabularyTreeNode>();
122        String schemaName = getDirectorySchema();
123        DocumentModelList results = getChildrenEntries();
124        Locale locale = FacesContext.getCurrentInstance().getViewRoot().getLocale();
125        for (DocumentModel result : results) {
126            if (result == null) {
127                continue;
128            }
129            String childIdendifier = result.getId();
130            String childLabel = computeLabel(locale, result, schemaName);
131            String childPath;
132            if ("".equals(path)) {
133                childPath = childIdendifier;
134            } else {
135                childPath = path + keySeparator + childIdendifier;
136            }
137            Comparable orderingValue = null;
138            if (!StringUtils.isBlank(orderingField)) {
139                orderingValue = (Comparable) result.getProperty(schemaName, orderingField);
140            }
141            children.add(new VocabularyTreeNode(level + 1, childIdendifier, childLabel, childPath, vocabularyName,
142                    getDirectoryService(), displayObsoleteEntries, keySeparator, orderingField, orderingValue));
143        }
144
145        // sort children
146        Comparator<? super VocabularyTreeNode> cmp;
147        if (StringUtils.isBlank(orderingField) || "label".equals(orderingField)) {
148            cmp = new LabelComparator(); // sort alphabetically
149        } else {
150            cmp = new OrderingComparator();
151        }
152        Collections.sort(children, cmp);
153
154        return children;
155    }
156
157    public static String computeLabel(Locale locale, DocumentModel entry, String schemaName) {
158        if (entry == null) {
159            return null;
160        }
161        String fieldName = LABEL_FIELD_PREFIX + locale.toString();
162        String label = null;
163        try {
164            label = (String) entry.getProperty(schemaName, fieldName);
165        } catch (PropertyException e) {
166        }
167        if (label == null) {
168            fieldName = LABEL_FIELD_PREFIX + locale.getLanguage();
169            try {
170                label = (String) entry.getProperty(schemaName, fieldName);
171            } catch (PropertyException e) {
172            }
173        }
174        if (label == null) {
175            fieldName = LABEL_FIELD_PREFIX + DEFAULT_LANGUAGE;
176            try {
177                label = (String) entry.getProperty(schemaName, fieldName);
178            } catch (PropertyException e) {
179            }
180        }
181        return label;
182    }
183
184    private class LabelComparator implements Comparator<VocabularyTreeNode> {
185        @Override
186        public int compare(VocabularyTreeNode o1, VocabularyTreeNode o2) {
187            return ObjectUtils.compare(o1.getLabel(), o2.getLabel());
188        }
189    }
190
191    private class OrderingComparator implements Comparator<VocabularyTreeNode> {
192        @Override
193        public int compare(VocabularyTreeNode o1, VocabularyTreeNode o2) {
194            if (o1.getOrdering() == null && o2.getOrdering() != null) {
195                return -1;
196            } else if (o1.getOrdering() != null && o2.getOrdering() == null) {
197                return 1;
198            } else if (o1.getOrdering() == o2.getOrdering()) {
199                return 0;
200            } else {
201                return o1.getOrdering().compareTo(o2.getOrdering());
202            }
203        }
204    }
205
206    protected DocumentModelList getChildrenEntries() {
207        if (childrenEntries != null) {
208            // memorized directory lookup since directory content is not
209            // suppose to change
210            // XXX: use the cache manager instead of field caching strategy
211            return childrenEntries;
212        }
213        try (Session session = getDirectorySession()) {
214            Map<String, Serializable> filter = new HashMap<String, Serializable>();
215
216            String directorySchema = getDirectorySchema();
217            SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
218            Schema schema = schemaManager.getSchema(directorySchema);
219            if (schema == null) {
220                throw new DirectoryException(directorySchema + " is not a registered directory");
221            }
222            if (level == 0 && schema.hasField(PARENT_FIELD_ID)) {
223                // filter on empty parent
224                filter.put(PARENT_FIELD_ID, "");
225            } else {
226                String[] bitsOfPath = StringUtils.split(path, keySeparator);
227                filter.put(PARENT_FIELD_ID, bitsOfPath[level - 1]);
228            }
229
230            if (!displayObsoleteEntries) {
231                filter.put(OBSOLETE_FIELD, Long.valueOf(0));
232            }
233
234            if (filter.isEmpty()) {
235                childrenEntries = session.getEntries();
236            } else {
237                childrenEntries = session.query(filter);
238            }
239            return childrenEntries;
240        }
241    }
242
243    public String getId() {
244        return id;
245    }
246
247    public String getLabel() {
248        return label;
249    }
250
251    public String getPath() {
252        return path;
253    }
254
255    public Comparable getOrdering() {
256        return orderingValue;
257    }
258
259    protected DirectoryService getDirectoryService() {
260        if (directoryService == null) {
261            directoryService = DirectoryHelper.getDirectoryService();
262        }
263        return directoryService;
264    }
265
266    protected String getDirectorySchema() {
267        return getDirectoryService().getDirectorySchema(vocabularyName);
268    }
269
270    protected Session getDirectorySession() {
271        return getDirectoryService().open(vocabularyName);
272    }
273
274}