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.CoreInstance;
032import org.nuxeo.ecm.core.api.CoreSession;
033import org.nuxeo.ecm.core.api.DocumentModel;
034import org.nuxeo.ecm.core.api.DocumentRef;
035import org.nuxeo.ecm.core.api.IdRef;
036import org.nuxeo.ecm.core.api.PathRef;
037import org.nuxeo.ecm.core.schema.types.resolver.AbstractObjectResolver;
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 extends AbstractObjectResolver implements ObjectResolver {
085
086    private static final long serialVersionUID = 1L;
087
088    public static final String NAME = "documentResolver";
089
090    public static final String PARAM_STORE = "store";
091
092    public static final String STORE_REPO_AND_PATH = "path";
093
094    /** Since 10.2 */
095    public static final String STORE_PATH_ONLY = "pathOnly";
096
097    public static final String STORE_REPO_AND_ID = "id";
098
099    /** Since 10.2 */
100    public static final String STORE_ID_ONLY = "idOnly";
101
102    public enum MODE {
103        /** Reference is a path prefixed with a repository. */
104        REPO_AND_PATH_REF,
105        /** Reference is an id prefixed with a repository. */
106        REPO_AND_ID_REF,
107        /** Reference is a path. */
108        PATH_ONLY_REF,
109        /** Reference is an id. */
110        ID_ONLY_REF,
111    }
112
113    private MODE mode = MODE.REPO_AND_ID_REF;
114
115    public MODE getMode() {
116        return mode;
117    }
118
119    private List<Class<?>> managedClasses = null;
120
121    @Override
122    public List<Class<?>> getManagedClasses() {
123        if (managedClasses == null) {
124            managedClasses = new ArrayList<>();
125            managedClasses.add(DocumentModel.class);
126        }
127        return managedClasses;
128    }
129
130    @Override
131    public void configure(Map<String, String> parameters) throws IllegalStateException {
132        super.configure(parameters);
133        String store = parameters.get(PARAM_STORE);
134        if (store == null) {
135            store = ""; // use default
136        }
137        switch (store) {
138        case STORE_PATH_ONLY:
139            mode = MODE.PATH_ONLY_REF;
140            break;
141        case STORE_ID_ONLY:
142            mode = MODE.ID_ONLY_REF;
143            break;
144        case STORE_REPO_AND_PATH:
145            mode = MODE.REPO_AND_PATH_REF;
146            break;
147        case STORE_REPO_AND_ID:
148        default:
149            mode = MODE.REPO_AND_ID_REF;
150            store = STORE_REPO_AND_ID;
151            break;
152        }
153        this.parameters.put(PARAM_STORE, store);
154    }
155
156    @Override
157    public String getName() {
158        checkConfig();
159        return NAME;
160    }
161
162    @Override
163    public boolean validate(Object value) {
164        return validate(value, null);
165    }
166
167    @Override
168    public boolean validate(Object value, Object context) {
169        if (!validation) {
170            return true;
171        }
172        MutableBoolean validated = new MutableBoolean();
173        resolve(value, context, (session, docRef) -> {
174            if (session.exists(docRef)) {
175                validated.setTrue();
176            }
177        });
178        return validated.isTrue();
179    }
180
181    @Override
182    public Object fetch(Object value) {
183        return fetch(value, null);
184    }
185
186    @Override
187    public Object fetch(Object value, Object context) {
188        MutableObject<DocumentModel> docHolder = new MutableObject<>();
189        resolve(value, context, (session, docRef) -> {
190            if (session.exists(docRef)) {
191                DocumentModel doc = session.getDocument(docRef);
192                // detach because we're about to close the session
193                doc.detach(true);
194                docHolder.setValue(doc);
195            }
196        });
197        return docHolder.getValue();
198    }
199
200    /**
201     * Resolves the value (in the context) into a session and docRef, and passes them to the resolver.
202     * <p>
203     * The resolver is not called if the value cannot be resolved.
204     */
205    protected void resolve(Object value, Object context, BiConsumer<CoreSession, DocumentRef> resolver) {
206        checkConfig();
207        if (!(value instanceof String)) {
208            return;
209        }
210        REF ref = REF.fromValue((String) value);
211        if (ref == null) {
212            return;
213        }
214        CoreSession session;
215        if (ref.repo != null) {
216            // we have an explicit repository name
217            if (context != null && ref.repo.equals(((CoreSession) context).getRepositoryName())) {
218                // if it's the same repository as the context session, use it directly
219                session = (CoreSession) context;
220            } else {
221                // otherwise open a new one
222                session = CoreInstance.getCoreSession(ref.repo);
223            }
224        } else {
225            // use session from context
226            session = (CoreSession) context;
227            if (session == null) {
228                // use the default repository if none is provided in the context
229                session = CoreInstance.getCoreSession(null);
230            }
231        }
232        DocumentRef docRef;
233        switch (mode) {
234        case ID_ONLY_REF:
235        case REPO_AND_ID_REF:
236            docRef = new IdRef(ref.ref);
237            break;
238        case PATH_ONLY_REF:
239        case REPO_AND_PATH_REF:
240            docRef = new PathRef(ref.ref);
241            break;
242        default:
243            // unknown ref type
244            return;
245        }
246        resolver.accept(session, docRef);
247    }
248
249    @Override
250    public <T> T fetch(Class<T> type, Object value) throws IllegalStateException {
251        checkConfig();
252        DocumentModel doc = (DocumentModel) fetch(value);
253        if (doc != null) {
254            if (type.isInstance(doc)) {
255                return type.cast(doc);
256            }
257            return doc.getAdapter(type);
258        }
259        return null;
260    }
261
262    @Override
263    public Serializable getReference(Object entity) throws IllegalStateException {
264        checkConfig();
265        if (entity instanceof DocumentModel) {
266            DocumentModel doc = (DocumentModel) entity;
267            switch (mode) {
268            case ID_ONLY_REF:
269                return doc.getId();
270            case PATH_ONLY_REF:
271                return doc.getPath().toString();
272            case REPO_AND_ID_REF:
273                return doc.getRepositoryName() + ":" + doc.getId();
274            case REPO_AND_PATH_REF:
275                return doc.getRepositoryName() + ":" + doc.getPath().toString();
276            }
277        }
278        return null;
279    }
280
281    @Override
282    public String getConstraintErrorMessage(Object invalidValue, Locale locale) {
283        checkConfig();
284        switch (mode) {
285        case ID_ONLY_REF:
286        case REPO_AND_ID_REF:
287            return Helper.getConstraintErrorMessage(this, "id", invalidValue, locale);
288        case PATH_ONLY_REF:
289        case REPO_AND_PATH_REF:
290            return Helper.getConstraintErrorMessage(this, "path", invalidValue, locale);
291        default:
292            return String.format("%s cannot resolve reference %s", getName(), invalidValue);
293        }
294    }
295
296
297    protected static final class REF {
298
299        protected String repo;
300
301        protected String ref;
302
303        protected REF() {
304        }
305
306        protected static REF fromValue(String value) {
307            String[] split = value.split(":");
308            if (split.length == 1) {
309                REF ref = new REF();
310                ref.repo = null; // caller will use context session, or the default repo
311                ref.ref = split[0];
312                return ref;
313            }
314            if (split.length == 2) {
315                REF ref = new REF();
316                ref.repo = split[0];
317                ref.ref = split[1];
318                return ref;
319            }
320            return null;
321        }
322
323    }
324
325}