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