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 * <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 extends AbstractObjectResolver 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 boolean hierarchical = false;
090
091    private String parentField = null;
092
093    private String separator = null;
094
095    private List<Class<?>> managedClasses = null;
096
097    private String directoryName;
098
099    @Override
100    public void configure(Map<String, String> parameters) throws IllegalArgumentException, IllegalStateException {
101        super.configure(parameters);
102        directoryName = parameters.get(PARAM_DIRECTORY);
103        if (directoryName != null) {
104            directoryName = directoryName.trim();
105        }
106        if (directoryName == null || directoryName.isEmpty()) {
107            throw new IllegalArgumentException("missing directory parameter. A directory name is necessary");
108        }
109        Directory directory = getDirectory();
110        idField = directory.getIdField();
111        schema = directory.getSchema();
112        if (schema.endsWith("xvocabulary")) {
113            hierarchical = true;
114            parentField = "parent";
115            separator = "/";
116        }
117        String parentFieldParam = StringUtils.trim(parameters.get(PARAM_PARENT_FIELD));
118        String separatorParam = StringUtils.trim(parameters.get(PARAM_SEPARATOR));
119        if (!StringUtils.isBlank(parentFieldParam) && !StringUtils.isBlank(separatorParam)) {
120            hierarchical = true;
121            parentField = parentFieldParam;
122            separator = separatorParam;
123        }
124        this.parameters.put(PARAM_DIRECTORY, directoryName);
125    }
126
127    @Override
128    public List<Class<?>> getManagedClasses() {
129        if (managedClasses == null) {
130            managedClasses = new ArrayList<>();
131            managedClasses.add(DirectoryEntry.class);
132        }
133        return managedClasses;
134    }
135
136    public Directory getDirectory() {
137        DirectoryService directoryService = Framework.getService(DirectoryService.class);
138        Directory directory = directoryService.getDirectory(directoryName);
139        if (directory == null) {
140            throw new IllegalArgumentException(String.format("the directory \"%s\" was not found", directoryName));
141        }
142        return directory;
143    }
144
145    @Override
146    public String getName() {
147        checkConfig();
148        return NAME;
149    }
150
151    @Override
152    public Object fetch(Object value) throws IllegalStateException {
153        checkConfig();
154        if (value instanceof String) {
155            String id = (String) value;
156            if (hierarchical) {
157                String[] ids = StringUtils.split(id, separator);
158                if (ids.length > 0) {
159                    id = ids[ids.length - 1];
160                } else {
161                    return null;
162                }
163            }
164            try (Session session = getDirectory().getSession()) {
165                String finalId = id; // Effectively final
166                DocumentModel doc = Framework.doPrivileged(() -> session.getEntry(finalId));
167                if (doc != null) {
168                    return new DirectoryEntry(directoryName, doc);
169                }
170                return null;
171            }
172        }
173        return null;
174    }
175
176    @Override
177    public <T> T fetch(Class<T> type, Object value) throws IllegalStateException {
178        checkConfig();
179        DirectoryEntry doc = (DirectoryEntry) fetch(value);
180        if (doc != null) {
181            if (type.isInstance(doc)) {
182                return type.cast(doc);
183            }
184            if (type.isInstance(doc.getDocumentModel())) {
185                return type.cast(doc.getDocumentModel());
186            }
187        }
188        return null;
189    }
190
191    @Override
192    public Serializable getReference(Object entity) throws IllegalStateException {
193        checkConfig();
194        DocumentModel entry = null;
195        if (entity != null) {
196            if (entity instanceof DirectoryEntry) {
197                entry = ((DirectoryEntry) entity).getDocumentModel();
198            } else if (entity instanceof DocumentModel) {
199                entry = (DocumentModel) entity;
200            }
201            if (entry != null) {
202                if (!entry.hasSchema(schema)) {
203                    return null;
204                }
205                String result = (String) entry.getProperty(schema, idField);
206                if (hierarchical) {
207                    String parent = (String) entry.getProperty(schema, parentField);
208                    try (Session session = getDirectory().getSession()) {
209                        while (parent != null) {
210                            String finalParent = parent; // Effectively final
211                            entry = Framework.doPrivileged(() -> session.getEntry(finalParent));
212                            if (entry == null) {
213                                break;
214                            }
215                            result = parent + separator + result;
216                            parent = (String) entry.getProperty(schema, parentField);
217                        }
218                    }
219                }
220                return result;
221            }
222        }
223        return null;
224    }
225
226    @Override
227    public String getConstraintErrorMessage(Object invalidValue, Locale locale) {
228        checkConfig();
229        return Helper.getConstraintErrorMessage(this, invalidValue, locale, directoryName);
230    }
231
232}