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                        DocumentModel doc;
179                        switch (mode) {
180                        case ID_REF:
181                            doc = session.getDocument(new IdRef(ref.ref));
182                            break;
183                        case PATH_REF:
184                            doc = session.getDocument(new PathRef(ref.ref));
185                            break;
186                        default:
187                            throw new UnsupportedOperationException();
188                        }
189                        // detach because we're about to close the session
190                        doc.detach(true);
191                        return doc;
192                    } catch (DocumentNotFoundException e) {
193                        return null;
194                    }
195                } catch (LocalException le) { // no such repo
196                    return null;
197                }
198            }
199        }
200        return null;
201    }
202
203    @Override
204    public <T> T fetch(Class<T> type, Object value) throws IllegalStateException {
205        checkConfig();
206        DocumentModel doc = (DocumentModel) fetch(value);
207        if (doc != null) {
208            if (type.isInstance(doc)) {
209                return type.cast(doc);
210            }
211            return doc.getAdapter(type);
212        }
213        return null;
214    }
215
216    @Override
217    public Serializable getReference(Object entity) throws IllegalStateException {
218        checkConfig();
219        if (entity != null && entity instanceof DocumentModel) {
220            DocumentModel doc = (DocumentModel) entity;
221            String repositoryName = doc.getRepositoryName();
222            if (repositoryName != null) {
223                switch (mode) {
224                case ID_REF:
225                    return repositoryName + ":" + doc.getId();
226                case PATH_REF:
227                    return repositoryName + ":" + doc.getPath().toString();
228                }
229            }
230        }
231        return null;
232    }
233
234    @Override
235    public String getConstraintErrorMessage(Object invalidValue, Locale locale) {
236        checkConfig();
237        switch (mode) {
238        case ID_REF:
239            return Helper.getConstraintErrorMessage(this, "id", invalidValue, locale);
240        case PATH_REF:
241            return Helper.getConstraintErrorMessage(this, "path", invalidValue, locale);
242        default:
243            return String.format("%s cannot resolve reference %s", getName(), invalidValue);
244        }
245    }
246
247    private void checkConfig() throws IllegalStateException {
248        if (parameters == null) {
249            throw new IllegalStateException(
250                    "you should call #configure(Map<String, String>) before. Please get this resolver throught ExternalReferenceService which is in charge of resolver configuration.");
251        }
252    }
253
254    protected static final class REF {
255
256        protected String repo;
257
258        protected String ref;
259
260        protected REF() {
261        }
262
263        protected static REF fromValue(String value) {
264            String[] split = value.split(":");
265            if (split.length == 1) {
266                REF ref = new REF();
267                ref.repo = DEFAULT_REPO_NAME;
268                ref.ref = split[0];
269                return ref;
270            }
271            if (split.length == 2) {
272                REF ref = new REF();
273                ref.repo = split[0];
274                ref.ref = split[1];
275                return ref;
276            }
277            return null;
278        }
279
280    }
281
282}