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.core.model;
021
022import java.io.Serializable;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Locale;
028import java.util.Map;
029
030import org.nuxeo.ecm.core.api.CoreInstance;
031import org.nuxeo.ecm.core.api.CoreSession;
032import org.nuxeo.ecm.core.api.DocumentModel;
033import org.nuxeo.ecm.core.api.DocumentNotFoundException;
034import org.nuxeo.ecm.core.api.IdRef;
035import org.nuxeo.ecm.core.api.PathRef;
036import org.nuxeo.ecm.core.api.local.LocalException;
037import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver;
038
039/**
040 * This {@link ObjectResolver} allows to manage integrity for fields containing {@link DocumentModel} references (id or
041 * path).
042 * <p>
043 * Resolved references must be either a path or an id, default mode is id. Storing path keep link with place in the
044 * Document hierarchy no matter which Document is referenced. Storing id track the Document no matter where the Document
045 * is stored.
046 * </p>
047 * <p>
048 * All references, id or path, are prefixed with the document expected repository name. For example :
049 * </p>
050 * <ul>
051 * <li>default:352c21bc-f908-4507-af99-411d3d84ee7d</li>
052 * <li>test:/path/to/my/doc</li>
053 * </ul>
054 * <p>
055 * The {@link #fetch(Object)} method returns {@link DocumentModel}. The {@link #fetch(Class, Object)} returns
056 * {@link DocumentModel} or specific document adapter.
057 * </p>
058 * <p>
059 * To use it, put the following code in your schema XSD :
060 * </p>
061 *
062 * <pre>
063 * {@code
064 * <!-- default resolver is an id based resolver -->
065 * <xs:simpleType name="favoriteDocument1">
066 *   <xs:restriction base="xs:string" ref:resolver="documentResolver" />
067 * </xs:simpleType>
068 *
069 * <!-- store id -->
070 * <xs:simpleType name="favoriteDocument2">
071 *   <xs:restriction base="xs:string" ref:resolver="documentResolver" ref:store="id" />
072 * </xs:simpleType>
073 *
074 * <!-- store path -->
075 * <xs:simpleType name="bestDocumentRepositoryPlace">
076 *   <xs:restriction base="xs:string" ref:resolver="documentResolver" ref:store="path" />
077 * </xs:simpleType>
078 * }
079 * </pre>
080 *
081 * @since 7.1
082 */
083public class DocumentModelResolver implements ObjectResolver {
084
085    private static final long serialVersionUID = 1L;
086
087    private static final String DEFAULT_REPO_NAME = "default";
088
089    public static final String NAME = "documentResolver";
090
091    public static final String PARAM_STORE = "store";
092
093    public static final String STORE_PATH_REF = "path";
094
095    public static final String STORE_ID_REF = "id";
096
097    private Map<String, Serializable> parameters;
098
099    public static enum MODE {
100        PATH_REF, ID_REF;
101    }
102
103    private MODE mode = MODE.ID_REF;
104
105    public MODE getMode() {
106        return mode;
107    }
108
109    private List<Class<?>> managedClasses = null;
110
111    @Override
112    public List<Class<?>> getManagedClasses() {
113        if (managedClasses == null) {
114            managedClasses = new ArrayList<Class<?>>();
115            managedClasses.add(DocumentModel.class);
116        }
117        return managedClasses;
118    }
119
120    @Override
121    public void configure(Map<String, String> parameters) throws IllegalStateException {
122        if (this.parameters != null) {
123            throw new IllegalStateException("cannot change configuration, may be already in use somewhere");
124        }
125        String store = parameters.get(PARAM_STORE);
126        if (store != null) {
127            if (STORE_ID_REF.equals(store)) {
128                mode = MODE.ID_REF;
129            } else if (STORE_PATH_REF.equals(store)) {
130                mode = MODE.PATH_REF;
131            }
132        }
133        this.parameters = new HashMap<String, Serializable>();
134        this.parameters.put(PARAM_STORE, mode == MODE.ID_REF ? STORE_ID_REF : STORE_PATH_REF);
135    }
136
137    @Override
138    public String getName() {
139        checkConfig();
140        return NAME;
141    }
142
143    @Override
144    public Map<String, Serializable> getParameters() {
145        checkConfig();
146        return Collections.unmodifiableMap(parameters);
147    }
148
149    @Override
150    public boolean validate(Object value) throws IllegalStateException {
151        checkConfig();
152        if (value != null && value instanceof String) {
153            REF ref = REF.fromValue((String) value);
154            if (ref != null) {
155                try (CoreSession session = CoreInstance.openCoreSession(ref.repo)) {
156                    switch (mode) {
157                    case ID_REF:
158                        return session.exists(new IdRef(ref.ref));
159                    case PATH_REF:
160                        return session.exists(new PathRef(ref.ref));
161                    }
162                } catch (LocalException le) { // no such repo
163                    return false;
164                }
165            }
166        }
167        return false;
168    }
169
170    @Override
171    public Object fetch(Object value) throws IllegalStateException {
172        checkConfig();
173        if (value != null && value instanceof String) {
174            REF ref = REF.fromValue((String) value);
175            if (ref != null) {
176                try (CoreSession session = CoreInstance.openCoreSession(ref.repo)) {
177                    try {
178                        switch (mode) {
179                        case ID_REF:
180                            return session.getDocument(new IdRef(ref.ref));
181                        case PATH_REF:
182                            return session.getDocument(new PathRef(ref.ref));
183                        }
184                    } catch (DocumentNotFoundException e) {
185                        return null;
186                    }
187                } catch (LocalException le) { // no such repo
188                    return null;
189                }
190            }
191        }
192        return null;
193    }
194
195    @Override
196    public <T> T fetch(Class<T> type, Object value) throws IllegalStateException {
197        checkConfig();
198        DocumentModel doc = (DocumentModel) fetch(value);
199        if (doc != null) {
200            if (type.isInstance(doc)) {
201                return type.cast(doc);
202            }
203            return doc.getAdapter(type);
204        }
205        return null;
206    }
207
208    @Override
209    public Serializable getReference(Object entity) throws IllegalStateException {
210        checkConfig();
211        if (entity != null && entity instanceof DocumentModel) {
212            DocumentModel doc = (DocumentModel) entity;
213            String repositoryName = doc.getRepositoryName();
214            if (repositoryName != null) {
215                switch (mode) {
216                case ID_REF:
217                    return repositoryName + ":" + doc.getId();
218                case PATH_REF:
219                    return repositoryName + ":" + doc.getPath().toString();
220                }
221            }
222        }
223        return null;
224    }
225
226    @Override
227    public String getConstraintErrorMessage(Object invalidValue, Locale locale) {
228        checkConfig();
229        switch (mode) {
230        case ID_REF:
231            return Helper.getConstraintErrorMessage(this, "id", invalidValue, locale);
232        case PATH_REF:
233            return Helper.getConstraintErrorMessage(this, "path", invalidValue, locale);
234        default:
235            return String.format("%s cannot resolve reference %s", getName(), invalidValue);
236        }
237    }
238
239    private void checkConfig() throws IllegalStateException {
240        if (parameters == null) {
241            throw new IllegalStateException(
242                    "you should call #configure(Map<String, String>) before. Please get this resolver throught ExternalReferenceService which is in charge of resolver configuration.");
243        }
244    }
245
246    protected static final class REF {
247
248        protected String repo;
249
250        protected String ref;
251
252        protected REF() {
253        }
254
255        protected static REF fromValue(String value) {
256            String[] split = value.split(":");
257            if (split.length == 1) {
258                REF ref = new REF();
259                ref.repo = DEFAULT_REPO_NAME;
260                ref.ref = split[0];
261                return ref;
262            }
263            if (split.length == 2) {
264                REF ref = new REF();
265                ref.repo = split[0];
266                ref.ref = split[1];
267                return ref;
268            }
269            return null;
270        }
271
272    }
273
274}