/*
* (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Nicolas Chapurlat <nchapurlat@nuxeo.com>
*/
package org.nuxeo.ecm.directory;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver;
import org.nuxeo.ecm.directory.api.DirectoryEntry;
import org.nuxeo.ecm.directory.api.DirectoryService;
import org.nuxeo.runtime.api.Framework;
/**
* This {@link ObjectResolver} allows to manage integrity for fields containing references to directory's entry.
* <p>
* References contains the directory entry id.
* </p>
* <p>
* To use it, put the following code in your schema XSD (don't forget the directory name):
* </p>
*
* <pre>
* {@code
* <xs:element name="carBrand">
* <xs:simpleType>
* <xs:restriction base="xs:string" ref:resolver="directoryResolver" ref:directory="carBrandsDirectory" />
* </xs:simpleType>
* </xs:element>
* </pre>
* <p>
* For hierarchical directories, which entries reference other entries. You can manage a specific reference containing
* the full entry path. You have to specify the parent field and the separator used to encode the reference.
* </p>
*
* <pre>
* {@code
* <xs:element name="coverage">
* <xs:simpleType>
* <xs:restriction base="xs:string" ref:resolver="directoryResolver" ref:directory="l10ncoverage" ref:parentField="parent" ref:separator="/" />
* </xs:simpleType>
* </xs:element>
* </pre>
* <p>
* It's not necessary to define parentField and separator for directory using schema ending by xvocabulary. The feature
* is automatically enable.
* </p>
*
* @since 7.1
*/
public class DirectoryEntryResolver implements ObjectResolver {
private static final long serialVersionUID = 1L;
public static final String NAME = "directoryResolver";
public static final String PARAM_DIRECTORY = "directory";
public static final String PARAM_PARENT_FIELD = "parentField";
public static final String PARAM_SEPARATOR = "separator";
private String idField;
private String schema;
private Map<String, Serializable> parameters;
private boolean hierarchical = false;
private String parentField = null;
private String separator = null;
private List<Class<?>> managedClasses = null;
private String directoryName;
/**
* the directory is transient - it's refetched on read object - see {@link #readObject(java.io.ObjectInputStream)}
*/
private transient Directory directory;
private transient DirectoryService directoryService;
@Override
public void configure(Map<String, String> parameters) throws IllegalArgumentException, IllegalStateException {
if (this.parameters != null) {
throw new IllegalStateException("cannot change configuration, may be already in use somewhere");
}
directoryName = parameters.get(PARAM_DIRECTORY);
if (directoryName != null) {
directoryName = directoryName.trim();
}
if (directoryName == null || directoryName.isEmpty()) {
throw new IllegalArgumentException("missing directory parameter. A directory name is necessary");
}
fetchDirectory();
idField = directory.getIdField();
schema = directory.getSchema();
if (schema.endsWith("xvocabulary")) {
hierarchical = true;
parentField = "parent";
separator = "/";
}
String parentFieldParam = StringUtils.trim(parameters.get(PARAM_PARENT_FIELD));
String separatorParam = StringUtils.trim(parameters.get(PARAM_SEPARATOR));
if (!StringUtils.isBlank(parentFieldParam) && !StringUtils.isBlank(separatorParam)) {
hierarchical = true;
parentField = parentFieldParam;
separator = separatorParam;
}
this.parameters = new HashMap<String, Serializable>();
this.parameters.put(PARAM_DIRECTORY, directory.getName());
}
@Override
public List<Class<?>> getManagedClasses() {
if (managedClasses == null) {
managedClasses = new ArrayList<Class<?>>();
managedClasses.add(DirectoryEntry.class);
}
return managedClasses;
}
private void fetchDirectory() {
directory = getDirectoryService().getDirectory(directoryName);
if (directory == null) {
throw new IllegalArgumentException(String.format("the directory \"%s\" was not found", directoryName));
}
}
public DirectoryService getDirectoryService() {
if (directoryService == null) {
directoryService = Framework.getService(DirectoryService.class);
}
return directoryService;
}
public Directory getDirectory() {
return directory;
}
public void setDirectory(Directory directory) {
this.directory = directory;
}
@Override
public String getName() {
checkConfig();
return NAME;
}
@Override
public Map<String, Serializable> getParameters() {
checkConfig();
return Collections.unmodifiableMap(parameters);
}
@Override
public boolean validate(Object value) throws IllegalStateException {
checkConfig();
return fetch(value) != null;
}
@Override
public Object fetch(Object value) throws IllegalStateException {
checkConfig();
if (value != null && value instanceof String) {
String id = (String) value;
if (hierarchical) {
String[] ids = StringUtils.split(id, separator);
if (ids.length > 0) {
id = ids[ids.length - 1];
} else {
return null;
}
}
try (Session session = directory.getSession()) {
DocumentModel doc = session.getEntry(id);
if (doc != null) {
return new DirectoryEntry(directory.getName(), doc);
}
return null;
}
}
return null;
}
@Override
public <T> T fetch(Class<T> type, Object value) throws IllegalStateException {
checkConfig();
DirectoryEntry doc = (DirectoryEntry) fetch(value);
if (doc != null) {
if (type.isInstance(doc)) {
return type.cast(doc);
}
if (type.isInstance(doc.getDocumentModel())) {
return type.cast(doc.getDocumentModel());
}
}
return null;
}
@Override
public Serializable getReference(Object entity) throws IllegalStateException {
checkConfig();
DocumentModel entry = null;
if (entity != null) {
if (entity instanceof DirectoryEntry) {
entry = ((DirectoryEntry) entity).getDocumentModel();
} else if (entity instanceof DocumentModel) {
entry = (DocumentModel) entity;
}
if (entry != null) {
if (!entry.hasSchema(schema)) {
return null;
}
String result = (String) entry.getProperty(schema, idField);
if (hierarchical) {
String parent = (String) entry.getProperty(schema, parentField);
try (Session session = directory.getSession()) {
while (parent != null) {
entry = session.getEntry(parent);
if (entry == null) {
break;
}
result = parent + separator + result;
parent = (String) entry.getProperty(schema, parentField);
}
}
}
return result;
}
}
return null;
}
@Override
public String getConstraintErrorMessage(Object invalidValue, Locale locale) {
checkConfig();
return Helper.getConstraintErrorMessage(this, invalidValue, locale, directory.getName());
}
private void checkConfig() throws IllegalStateException {
if (parameters == null) {
throw new IllegalStateException(
"you should call #configure(Map<String, String>) before. Please get this resolver throught ExternalReferenceService which is in charge of resolver configuration.");
}
}
/**
* Refetch the directory which is transient.
*
* @since 7.10
*/
private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
fetchDirectory();
}
}