001/*
002 * (C) Copyright 2014-2018 Nuxeo (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.Serializable;
023import java.util.ArrayList;
024import java.util.List;
025import java.util.Locale;
026import java.util.Map;
027
028import org.apache.commons.lang3.StringUtils;
029import org.nuxeo.ecm.core.api.DocumentModel;
030import org.nuxeo.ecm.core.schema.types.resolver.AbstractObjectResolver;
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 * To use it, put the following code in your schema XSD (don't forget the directory name):
042 *
043 * <pre>
044 * {@code
045 * <xs:element name="carBrand">
046 *   <xs:simpleType>
047 *     <xs:restriction base="xs:string" ref:resolver="directoryResolver" ref:directory="carBrandsDirectory" />
048 *   </xs:simpleType>
049 * </xs:element>
050 * }
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 *
056 * <pre>
057 * {@code
058 * <xs:element name="coverage">
059 *   <xs:simpleType>
060 *     <xs:restriction base="xs:string" ref:resolver="directoryResolver" ref:directory="l10ncoverage" ref:parentField="parent" ref:separator="/" />
061 *   </xs:simpleType>
062 * </xs:element>
063 * }
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 *
069 * @since 7.1
070 */
071public class DirectoryEntryResolver extends AbstractObjectResolver implements ObjectResolver {
072
073    private static final long serialVersionUID = 1L;
074
075    public static final String NAME = "directoryResolver";
076
077    public static final String PARAM_DIRECTORY = "directory";
078
079    public static final String PARAM_PARENT_FIELD = "parentField";
080
081    public static final String PARAM_SEPARATOR = "separator";
082
083    private String idField;
084
085    private String schema;
086
087    private boolean hierarchical = false;
088
089    private String parentField = null;
090
091    private String separator = null;
092
093    private List<Class<?>> managedClasses = null;
094
095    private String directoryName;
096
097    @Override
098    public void configure(Map<String, String> parameters) throws IllegalArgumentException, IllegalStateException {
099        super.configure(parameters);
100        directoryName = parameters.get(PARAM_DIRECTORY);
101        if (directoryName != null) {
102            directoryName = directoryName.trim();
103        }
104        if (directoryName == null || directoryName.isEmpty()) {
105            throw new IllegalArgumentException("missing directory parameter. A directory name is necessary");
106        }
107        Directory directory = getDirectory();
108        idField = directory.getIdField();
109        schema = directory.getSchema();
110        if (schema.endsWith("xvocabulary")) {
111            hierarchical = true;
112            parentField = "parent";
113            separator = "/";
114        }
115        String parentFieldParam = StringUtils.trim(parameters.get(PARAM_PARENT_FIELD));
116        String separatorParam = StringUtils.trim(parameters.get(PARAM_SEPARATOR));
117        if (!StringUtils.isBlank(parentFieldParam) && !StringUtils.isBlank(separatorParam)) {
118            hierarchical = true;
119            parentField = parentFieldParam;
120            separator = separatorParam;
121        }
122        this.parameters.put(PARAM_DIRECTORY, directoryName);
123    }
124
125    @Override
126    public List<Class<?>> getManagedClasses() {
127        if (managedClasses == null) {
128            managedClasses = new ArrayList<>();
129            managedClasses.add(DirectoryEntry.class);
130        }
131        return managedClasses;
132    }
133
134    public Directory getDirectory() {
135        DirectoryService directoryService = Framework.getService(DirectoryService.class);
136        Directory directory = directoryService.getDirectory(directoryName);
137        if (directory == null) {
138            throw new IllegalArgumentException(String.format("the directory \"%s\" was not found", directoryName));
139        }
140        return directory;
141    }
142
143    @Override
144    public String getName() {
145        checkConfig();
146        return NAME;
147    }
148
149    @Override
150    public Object fetch(Object value) throws IllegalStateException {
151        checkConfig();
152        if (value instanceof String) {
153            String id = (String) value;
154            if (hierarchical) {
155                String[] ids = StringUtils.split(id, separator);
156                if (ids.length > 0) {
157                    id = ids[ids.length - 1];
158                } else {
159                    return null;
160                }
161            }
162            try (Session session = getDirectory().getSession()) {
163                String finalId = id; // Effectively final
164                DocumentModel doc = Framework.doPrivileged(() -> session.getEntry(finalId));
165                if (doc != null) {
166                    return new DirectoryEntry(directoryName, doc);
167                }
168                return null;
169            }
170        }
171        return null;
172    }
173
174    @Override
175    public <T> T fetch(Class<T> type, Object value) throws IllegalStateException {
176        checkConfig();
177        DirectoryEntry doc = (DirectoryEntry) fetch(value);
178        if (doc != null) {
179            if (type.isInstance(doc)) {
180                return type.cast(doc);
181            }
182            if (type.isInstance(doc.getDocumentModel())) {
183                return type.cast(doc.getDocumentModel());
184            }
185        }
186        return null;
187    }
188
189    @Override
190    public Serializable getReference(Object entity) throws IllegalStateException {
191        checkConfig();
192        DocumentModel entry = null;
193        if (entity != null) {
194            if (entity instanceof DirectoryEntry) {
195                entry = ((DirectoryEntry) entity).getDocumentModel();
196            } else if (entity instanceof DocumentModel) {
197                entry = (DocumentModel) entity;
198            }
199            if (entry != null) {
200                if (!entry.hasSchema(schema)) {
201                    return null;
202                }
203                String result = (String) entry.getProperty(schema, idField);
204                if (hierarchical) {
205                    String parent = (String) entry.getProperty(schema, parentField);
206                    try (Session session = getDirectory().getSession()) {
207                        while (parent != null) {
208                            String finalParent = parent; // Effectively final
209                            entry = Framework.doPrivileged(() -> session.getEntry(finalParent));
210                            if (entry == null) {
211                                break;
212                            }
213                            result = parent + separator + result;
214                            parent = (String) entry.getProperty(schema, parentField);
215                        }
216                    }
217                }
218                return result;
219            }
220        }
221        return null;
222    }
223
224    @Override
225    public String getConstraintErrorMessage(Object invalidValue, Locale locale) {
226        checkConfig();
227        return Helper.getConstraintErrorMessage(this, invalidValue, locale, directoryName);
228    }
229
230}