001/*
002 * (C) Copyright 2014-2018 Nuxeo (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.List;
025import java.util.Locale;
026import java.util.Map;
027import java.util.function.BiConsumer;
028
029import org.apache.commons.lang3.mutable.MutableBoolean;
030import org.apache.commons.lang3.mutable.MutableObject;
031import org.nuxeo.ecm.core.api.CloseableCoreSession;
032import org.nuxeo.ecm.core.api.CoreInstance;
033import org.nuxeo.ecm.core.api.CoreSession;
034import org.nuxeo.ecm.core.api.DocumentModel;
035import org.nuxeo.ecm.core.api.DocumentRef;
036import org.nuxeo.ecm.core.api.IdRef;
037import org.nuxeo.ecm.core.api.PathRef;
038import org.nuxeo.ecm.core.api.local.LocalException;
039import org.nuxeo.ecm.core.schema.types.resolver.AbstractObjectResolver;
040import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver;
041
042/**
043 * This {@link ObjectResolver} allows to manage integrity for fields containing {@link DocumentModel} references (id or
044 * path).
045 * <p>
046 * Resolved references must be either a path or an id, default mode is id. Storing path keep link with place in the
047 * Document hierarchy no matter which Document is referenced. Storing id track the Document no matter where the Document
048 * is stored.
049 * </p>
050 * <p>
051 * All references, id or path, are prefixed with the document expected repository name. For example :
052 * </p>
053 * <ul>
054 * <li>default:352c21bc-f908-4507-af99-411d3d84ee7d</li>
055 * <li>test:/path/to/my/doc</li>
056 * </ul>
057 * <p>
058 * The {@link #fetch(Object)} method returns {@link DocumentModel}. The {@link #fetch(Class, Object)} returns
059 * {@link DocumentModel} or specific document adapter.
060 * </p>
061 * <p>
062 * To use it, put the following code in your schema XSD :
063 * </p>
064 *
065 * <pre>
066 * {@code
067 * <!-- default resolver is an id based resolver -->
068 * <xs:simpleType name="favoriteDocument1">
069 *   <xs:restriction base="xs:string" ref:resolver="documentResolver" />
070 * </xs:simpleType>
071 *
072 * <!-- store id -->
073 * <xs:simpleType name="favoriteDocument2">
074 *   <xs:restriction base="xs:string" ref:resolver="documentResolver" ref:store="id" />
075 * </xs:simpleType>
076 *
077 * <!-- store path -->
078 * <xs:simpleType name="bestDocumentRepositoryPlace">
079 *   <xs:restriction base="xs:string" ref:resolver="documentResolver" ref:store="path" />
080 * </xs:simpleType>
081 * }
082 * </pre>
083 *
084 * @since 7.1
085 */
086public class DocumentModelResolver extends AbstractObjectResolver implements ObjectResolver {
087
088    private static final long serialVersionUID = 1L;
089
090    public static final String NAME = "documentResolver";
091
092    public static final String PARAM_STORE = "store";
093
094    public static final String STORE_REPO_AND_PATH = "path";
095
096    /** Since 10.2 */
097    public static final String STORE_PATH_ONLY = "pathOnly";
098
099    public static final String STORE_REPO_AND_ID = "id";
100
101    /** Since 10.2 */
102    public static final String STORE_ID_ONLY = "idOnly";
103
104    public enum MODE {
105        /** Reference is a path prefixed with a repository. */
106        REPO_AND_PATH_REF,
107        /** Reference is an id prefixed with a repository. */
108        REPO_AND_ID_REF,
109        /** Reference is a path. */
110        PATH_ONLY_REF,
111        /** Reference is an id. */
112        ID_ONLY_REF,
113    }
114
115    private MODE mode = MODE.REPO_AND_ID_REF;
116
117    public MODE getMode() {
118        return mode;
119    }
120
121    private List<Class<?>> managedClasses = null;
122
123    @Override
124    public List<Class<?>> getManagedClasses() {
125        if (managedClasses == null) {
126            managedClasses = new ArrayList<>();
127            managedClasses.add(DocumentModel.class);
128        }
129        return managedClasses;
130    }
131
132    @Override
133    public void configure(Map<String, String> parameters) throws IllegalStateException {
134        super.configure(parameters);
135        String store = parameters.get(PARAM_STORE);
136        if (store == null) {
137            store = ""; // use default
138        }
139        switch (store) {
140        case STORE_PATH_ONLY:
141            mode = MODE.PATH_ONLY_REF;
142            break;
143        case STORE_ID_ONLY:
144            mode = MODE.ID_ONLY_REF;
145            break;
146        case STORE_REPO_AND_PATH:
147            mode = MODE.REPO_AND_PATH_REF;
148            break;
149        case STORE_REPO_AND_ID:
150        default:
151            mode = MODE.REPO_AND_ID_REF;
152            store = STORE_REPO_AND_ID;
153            break;
154        }
155        this.parameters.put(PARAM_STORE, store);
156    }
157
158    @Override
159    public String getName() {
160        checkConfig();
161        return NAME;
162    }
163
164    @Override
165    public boolean validate(Object value) {
166        return validate(value, null);
167    }
168
169    @Override
170    public boolean validate(Object value, Object context) {
171        MutableBoolean validated = new MutableBoolean();
172        resolve(value, context, (session, docRef) -> {
173            if (session.exists(docRef)) {
174                validated.setTrue();
175            }
176        });
177        return validated.isTrue();
178    }
179
180    @Override
181    public Object fetch(Object value) {
182        return fetch(value, null);
183    }
184
185    @Override
186    public Object fetch(Object value, Object context) {
187        MutableObject<DocumentModel> docHolder = new MutableObject<>();
188        resolve(value, context, (session, docRef) -> {
189            if (session.exists(docRef)) {
190                DocumentModel doc = session.getDocument(docRef);
191                // detach because we're about to close the session
192                doc.detach(true);
193                docHolder.setValue(doc);
194            }
195        });
196        return docHolder.getValue();
197    }
198
199    /**
200     * Resolves the value (in the context) into a session and docRef, and passes them to the resolver.
201     * <p>
202     * The resolver is not called if the value cannot be resolved.
203     */
204    protected void resolve(Object value, Object context, BiConsumer<CoreSession, DocumentRef> resolver) {
205        checkConfig();
206        if (!(value instanceof String)) {
207            return;
208        }
209        REF ref = REF.fromValue((String) value);
210        if (ref == null) {
211            return;
212        }
213        CloseableCoreSession closeableCoreSession = null;
214        try {
215            CoreSession session;
216            try {
217                if (ref.repo != null) {
218                    // we have an explicit repository name
219                    if (context != null && ref.repo.equals(((CoreSession) context).getRepositoryName())) {
220                        // if it's the same repository as the context session, use it directly
221                        session = (CoreSession) context;
222                    } else {
223                        // otherwise open a new one
224                        closeableCoreSession = CoreInstance.openCoreSession(ref.repo);
225                        session = closeableCoreSession;
226                    }
227                } else {
228                    // use session from context
229                    session = (CoreSession) context;
230                    if (session == null) {
231                        // use the default repository if none is provided in the context
232                        closeableCoreSession = CoreInstance.openCoreSession(null);
233                        session = closeableCoreSession;
234                    }
235                }
236            } catch (LocalException e) {
237                // no such repository
238                return;
239            }
240            DocumentRef docRef;
241            switch (mode) {
242            case ID_ONLY_REF:
243            case REPO_AND_ID_REF:
244                docRef = new IdRef(ref.ref);
245                break;
246            case PATH_ONLY_REF:
247            case REPO_AND_PATH_REF:
248                docRef = new PathRef(ref.ref);
249                break;
250            default:
251                // unknown ref type
252                return;
253            }
254            resolver.accept(session, docRef);
255        } finally {
256            if (closeableCoreSession != null) {
257                closeableCoreSession.close();
258            }
259        }
260    }
261
262    @Override
263    public <T> T fetch(Class<T> type, Object value) throws IllegalStateException {
264        checkConfig();
265        DocumentModel doc = (DocumentModel) fetch(value);
266        if (doc != null) {
267            if (type.isInstance(doc)) {
268                return type.cast(doc);
269            }
270            return doc.getAdapter(type);
271        }
272        return null;
273    }
274
275    @Override
276    public Serializable getReference(Object entity) throws IllegalStateException {
277        checkConfig();
278        if (entity instanceof DocumentModel) {
279            DocumentModel doc = (DocumentModel) entity;
280            switch (mode) {
281            case ID_ONLY_REF:
282                return doc.getId();
283            case PATH_ONLY_REF:
284                return doc.getPath().toString();
285            case REPO_AND_ID_REF:
286                return doc.getRepositoryName() + ":" + doc.getId();
287            case REPO_AND_PATH_REF:
288                return doc.getRepositoryName() + ":" + doc.getPath().toString();
289            }
290        }
291        return null;
292    }
293
294    @Override
295    public String getConstraintErrorMessage(Object invalidValue, Locale locale) {
296        checkConfig();
297        switch (mode) {
298        case ID_ONLY_REF:
299        case REPO_AND_ID_REF:
300            return Helper.getConstraintErrorMessage(this, "id", invalidValue, locale);
301        case PATH_ONLY_REF:
302        case REPO_AND_PATH_REF:
303            return Helper.getConstraintErrorMessage(this, "path", invalidValue, locale);
304        default:
305            return String.format("%s cannot resolve reference %s", getName(), invalidValue);
306        }
307    }
308
309
310    protected static final class REF {
311
312        protected String repo;
313
314        protected String ref;
315
316        protected REF() {
317        }
318
319        protected static REF fromValue(String value) {
320            String[] split = value.split(":");
321            if (split.length == 1) {
322                REF ref = new REF();
323                ref.repo = null; // caller will use context session, or the default repo
324                ref.ref = split[0];
325                return ref;
326            }
327            if (split.length == 2) {
328                REF ref = new REF();
329                ref.repo = split[0];
330                ref.ref = split[1];
331                return ref;
332            }
333            return null;
334        }
335
336    }
337
338}