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