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