/*
* (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.core.model;
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.nuxeo.ecm.core.api.CoreInstance;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentNotFoundException;
import org.nuxeo.ecm.core.api.IdRef;
import org.nuxeo.ecm.core.api.PathRef;
import org.nuxeo.ecm.core.api.local.LocalException;
import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver;
/**
* This {@link ObjectResolver} allows to manage integrity for fields containing {@link DocumentModel} references (id or
* path).
* <p>
* Resolved references must be either a path or an id, default mode is id. Storing path keep link with place in the
* Document hierarchy no matter which Document is referenced. Storing id track the Document no matter where the Document
* is stored.
* </p>
* <p>
* All references, id or path, are prefixed with the document expected repository name. For example :
* </p>
* <ul>
* <li>default:352c21bc-f908-4507-af99-411d3d84ee7d</li>
* <li>test:/path/to/my/doc</li>
* </ul>
* <p>
* The {@link #fetch(Object)} method returns {@link DocumentModel}. The {@link #fetch(Class, Object)} returns
* {@link DocumentModel} or specific document adapter.
* </p>
* <p>
* To use it, put the following code in your schema XSD :
* </p>
*
* <pre>
* {@code
* <!-- default resolver is an id based resolver -->
* <xs:simpleType name="favoriteDocument1">
* <xs:restriction base="xs:string" ref:resolver="documentResolver" />
* </xs:simpleType>
*
* <!-- store id -->
* <xs:simpleType name="favoriteDocument2">
* <xs:restriction base="xs:string" ref:resolver="documentResolver" ref:store="id" />
* </xs:simpleType>
*
* <!-- store path -->
* <xs:simpleType name="bestDocumentRepositoryPlace">
* <xs:restriction base="xs:string" ref:resolver="documentResolver" ref:store="path" />
* </xs:simpleType>
* }
* </pre>
*
* @since 7.1
*/
public class DocumentModelResolver implements ObjectResolver {
private static final long serialVersionUID = 1L;
private static final String DEFAULT_REPO_NAME = "default";
public static final String NAME = "documentResolver";
public static final String PARAM_STORE = "store";
public static final String STORE_PATH_REF = "path";
public static final String STORE_ID_REF = "id";
private Map<String, Serializable> parameters;
public static enum MODE {
PATH_REF, ID_REF;
}
private MODE mode = MODE.ID_REF;
public MODE getMode() {
return mode;
}
private List<Class<?>> managedClasses = null;
@Override
public List<Class<?>> getManagedClasses() {
if (managedClasses == null) {
managedClasses = new ArrayList<Class<?>>();
managedClasses.add(DocumentModel.class);
}
return managedClasses;
}
@Override
public void configure(Map<String, String> parameters) throws IllegalStateException {
if (this.parameters != null) {
throw new IllegalStateException("cannot change configuration, may be already in use somewhere");
}
String store = parameters.get(PARAM_STORE);
if (store != null) {
if (STORE_ID_REF.equals(store)) {
mode = MODE.ID_REF;
} else if (STORE_PATH_REF.equals(store)) {
mode = MODE.PATH_REF;
}
}
this.parameters = new HashMap<String, Serializable>();
this.parameters.put(PARAM_STORE, mode == MODE.ID_REF ? STORE_ID_REF : STORE_PATH_REF);
}
@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();
if (value != null && value instanceof String) {
REF ref = REF.fromValue((String) value);
if (ref != null) {
try (CoreSession session = CoreInstance.openCoreSession(ref.repo)) {
switch (mode) {
case ID_REF:
return session.exists(new IdRef(ref.ref));
case PATH_REF:
return session.exists(new PathRef(ref.ref));
}
} catch (LocalException le) { // no such repo
return false;
}
}
}
return false;
}
@Override
public Object fetch(Object value) throws IllegalStateException {
checkConfig();
if (value != null && value instanceof String) {
REF ref = REF.fromValue((String) value);
if (ref != null) {
try (CoreSession session = CoreInstance.openCoreSession(ref.repo)) {
try {
DocumentModel doc;
switch (mode) {
case ID_REF:
doc = session.getDocument(new IdRef(ref.ref));
break;
case PATH_REF:
doc = session.getDocument(new PathRef(ref.ref));
break;
default:
throw new UnsupportedOperationException();
}
// detach because we're about to close the session
doc.detach(true);
return doc;
} catch (DocumentNotFoundException e) {
return null;
}
} catch (LocalException le) { // no such repo
return null;
}
}
}
return null;
}
@Override
public <T> T fetch(Class<T> type, Object value) throws IllegalStateException {
checkConfig();
DocumentModel doc = (DocumentModel) fetch(value);
if (doc != null) {
if (type.isInstance(doc)) {
return type.cast(doc);
}
return doc.getAdapter(type);
}
return null;
}
@Override
public Serializable getReference(Object entity) throws IllegalStateException {
checkConfig();
if (entity != null && entity instanceof DocumentModel) {
DocumentModel doc = (DocumentModel) entity;
String repositoryName = doc.getRepositoryName();
if (repositoryName != null) {
switch (mode) {
case ID_REF:
return repositoryName + ":" + doc.getId();
case PATH_REF:
return repositoryName + ":" + doc.getPath().toString();
}
}
}
return null;
}
@Override
public String getConstraintErrorMessage(Object invalidValue, Locale locale) {
checkConfig();
switch (mode) {
case ID_REF:
return Helper.getConstraintErrorMessage(this, "id", invalidValue, locale);
case PATH_REF:
return Helper.getConstraintErrorMessage(this, "path", invalidValue, locale);
default:
return String.format("%s cannot resolve reference %s", getName(), invalidValue);
}
}
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.");
}
}
protected static final class REF {
protected String repo;
protected String ref;
protected REF() {
}
protected static REF fromValue(String value) {
String[] split = value.split(":");
if (split.length == 1) {
REF ref = new REF();
ref.repo = DEFAULT_REPO_NAME;
ref.ref = split[0];
return ref;
}
if (split.length == 2) {
REF ref = new REF();
ref.repo = split[0];
ref.ref = split[1];
return ref;
}
return null;
}
}
}