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