001/*
002 * (C) Copyright 2014 Nuxeo SA (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-2.1.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 *     Nicolas Chapurlat <nchapurlat@nuxeo.com>
016 */
017
018package org.nuxeo.ecm.directory;
019
020import java.io.Serializable;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Locale;
026import java.util.Map;
027
028import org.apache.commons.lang.StringUtils;
029import org.nuxeo.ecm.core.api.DocumentModel;
030import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver;
031import org.nuxeo.ecm.directory.api.DirectoryEntry;
032import org.nuxeo.ecm.directory.api.DirectoryService;
033import org.nuxeo.runtime.api.Framework;
034
035/**
036 * This {@link ObjectResolver} allows to manage integrity for fields containing references to directory's entry.
037 * <p>
038 * References contains the directory entry id.
039 * </p>
040 * <p>
041 * To use it, put the following code in your schema XSD (don't forget the directory name):
042 * </p>
043 *
044 * <pre>
045 * {@code
046 * <xs:element name="carBrand">
047 *   <xs:simpleType>
048 *     <xs:restriction base="xs:string" ref:resolver="directoryResolver" ref:directory="carBrandsDirectory" />
049 *   </xs:simpleType>
050 * </xs:element>
051 * </pre>
052 * <p>
053 * For hierarchical directories, which entries reference other entries. You can manage a specific reference containing
054 * the full entry path. You have to specify the parent field and the separator used to encode the reference.
055 * </p>
056 *
057 * <pre>
058 * {@code
059 * <xs:element name="coverage">
060 *   <xs:simpleType>
061 *     <xs:restriction base="xs:string" ref:resolver="directoryResolver" ref:directory="l10ncoverage" ref:parentField="parent" ref:separator="/" />
062 *   </xs:simpleType>
063 * </xs:element>
064 * </pre>
065 * <p>
066 * It's not necessary to define parentField and separator for directory using schema ending by xvocabulary. The feature
067 * is automatically enable.
068 * </p>
069 *
070 * @since 7.1
071 */
072public class DirectoryEntryResolver implements ObjectResolver {
073
074    public static final String NAME = "directoryResolver";
075
076    public static final String PARAM_DIRECTORY = "directory";
077
078    public static final String PARAM_PARENT_FIELD = "parentField";
079
080    public static final String PARAM_SEPARATOR = "separator";
081
082    private String idField;
083
084    private String schema;
085
086    private Directory directory;
087
088    private Map<String, Serializable> parameters;
089
090    private DirectoryService directoryService;
091
092    private boolean hierarchical = false;
093
094    private String parentField = null;
095
096    private String separator = null;
097
098    public DirectoryService getDirectoryService() {
099        if (directoryService == null) {
100            directoryService = Framework.getService(DirectoryService.class);
101        }
102        return directoryService;
103    }
104
105    public Directory getDirectory() {
106        return directory;
107    }
108
109    public void setDirectory(Directory directory) {
110        this.directory = directory;
111    }
112
113    private List<Class<?>> managedClasses = null;
114
115    @Override
116    public List<Class<?>> getManagedClasses() {
117        if (managedClasses == null) {
118            managedClasses = new ArrayList<Class<?>>();
119            managedClasses.add(DirectoryEntry.class);
120        }
121        return managedClasses;
122    }
123
124    @Override
125    public void configure(Map<String, String> parameters) throws IllegalArgumentException, IllegalStateException {
126        if (this.parameters != null) {
127            throw new IllegalStateException("cannot change configuration, may be already in use somewhere");
128        }
129        String directoryName = parameters.get(PARAM_DIRECTORY);
130        if (directoryName != null) {
131            directoryName = directoryName.trim();
132        }
133        if (directoryName == null || directoryName.isEmpty()) {
134            throw new IllegalArgumentException("missing directory parameter. A directory name is necessary");
135        }
136        directory = getDirectoryService().getDirectory(directoryName);
137        if (directory == null) {
138            throw new IllegalArgumentException(String.format("the directory \"%s\" was not found", directoryName));
139        }
140        idField = directory.getIdField();
141        schema = directory.getSchema();
142        if (schema.endsWith("xvocabulary")) {
143            hierarchical = true;
144            parentField = "parent";
145            separator = "/";
146        }
147        String parentFieldParam = StringUtils.trim(parameters.get(PARAM_PARENT_FIELD));
148        String separatorParam = StringUtils.trim(parameters.get(PARAM_SEPARATOR));
149        if (!StringUtils.isBlank(parentFieldParam) && !StringUtils.isBlank(separatorParam)) {
150            hierarchical = true;
151            parentField = parentFieldParam;
152            separator = separatorParam;
153        }
154        this.parameters = new HashMap<String, Serializable>();
155        this.parameters.put(PARAM_DIRECTORY, directory.getName());
156    }
157
158    @Override
159    public String getName() {
160        checkConfig();
161        return NAME;
162    }
163
164    @Override
165    public Map<String, Serializable> getParameters() {
166        checkConfig();
167        return Collections.unmodifiableMap(parameters);
168    }
169
170    @Override
171    public boolean validate(Object value) throws IllegalStateException {
172        checkConfig();
173        return fetch(value) != null;
174    }
175
176    @Override
177    public Object fetch(Object value) throws IllegalStateException {
178        checkConfig();
179        if (value != null && value instanceof String) {
180            String id = (String) value;
181            if (hierarchical) {
182                String[] ids = StringUtils.split(id, separator);
183                if (ids.length > 0) {
184                    id = ids[ids.length - 1];
185                } else {
186                    return null;
187                }
188            }
189            try (Session session = directory.getSession()) {
190                DocumentModel doc = session.getEntry(id);
191                if (doc != null) {
192                    return new DirectoryEntry(directory.getName(), doc);
193                }
194                return null;
195            }
196        }
197        return null;
198    }
199
200    @Override
201    public <T> T fetch(Class<T> type, Object value) throws IllegalStateException {
202        checkConfig();
203        DirectoryEntry doc = (DirectoryEntry) fetch(value);
204        if (doc != null) {
205            if (type.isInstance(doc)) {
206                return type.cast(doc);
207            }
208            if (type.isInstance(doc.getDocumentModel())) {
209                return type.cast(doc.getDocumentModel());
210            }
211        }
212        return null;
213    }
214
215    @Override
216    public Serializable getReference(Object entity) throws IllegalStateException {
217        checkConfig();
218        DocumentModel entry = null;
219        if (entity != null) {
220            if (entity instanceof DirectoryEntry) {
221                entry = ((DirectoryEntry) entity).getDocumentModel();
222            } else if (entity instanceof DocumentModel) {
223                entry = (DocumentModel) entity;
224            }
225            if (entry != null) {
226                if (!entry.hasSchema(schema)) {
227                    return null;
228                }
229                String result = (String) entry.getProperty(schema, idField);
230                if (hierarchical) {
231                    String parent = (String) entry.getProperty(schema, parentField);
232                    try (Session session = directory.getSession()) {
233                        while (parent != null) {
234                            entry = session.getEntry(parent);
235                            if (entry == null) {
236                                break;
237                            }
238                            result = parent + separator + result;
239                            parent = (String) entry.getProperty(schema, parentField);
240                        }
241                    }
242                }
243                return result;
244            }
245        }
246        return null;
247    }
248
249    @Override
250    public String getConstraintErrorMessage(Object invalidValue, Locale locale) {
251        checkConfig();
252        return Helper.getConstraintErrorMessage(this, invalidValue, locale, directory.getName());
253    }
254
255    private void checkConfig() throws IllegalStateException {
256        if (parameters == null) {
257            throw new IllegalStateException(
258                    "you should call #configure(Map<String, String>) before. Please get this resolver throught ExternalReferenceService which is in charge of resolver configuration.");
259        }
260    }
261
262}