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