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 String DEFAULT_REPO_NAME = "default";
084
085    public static final String NAME = "documentResolver";
086
087    public static final String PARAM_STORE = "store";
088
089    public static final String STORE_PATH_REF = "path";
090
091    public static final String STORE_ID_REF = "id";
092
093    private Map<String, Serializable> parameters;
094
095    public static enum MODE {
096        PATH_REF, ID_REF;
097    }
098
099    private MODE mode = MODE.ID_REF;
100
101    public MODE getMode() {
102        return mode;
103    }
104
105    private List<Class<?>> managedClasses = null;
106
107    @Override
108    public List<Class<?>> getManagedClasses() {
109        if (managedClasses == null) {
110            managedClasses = new ArrayList<Class<?>>();
111            managedClasses.add(DocumentModel.class);
112        }
113        return managedClasses;
114    }
115
116    @Override
117    public void configure(Map<String, String> parameters) throws IllegalStateException {
118        if (this.parameters != null) {
119            throw new IllegalStateException("cannot change configuration, may be already in use somewhere");
120        }
121        String store = parameters.get(PARAM_STORE);
122        if (store != null) {
123            if (STORE_ID_REF.equals(store)) {
124                mode = MODE.ID_REF;
125            } else if (STORE_PATH_REF.equals(store)) {
126                mode = MODE.PATH_REF;
127            }
128        }
129        this.parameters = new HashMap<String, Serializable>();
130        this.parameters.put(PARAM_STORE, mode == MODE.ID_REF ? STORE_ID_REF : STORE_PATH_REF);
131    }
132
133    @Override
134    public String getName() {
135        checkConfig();
136        return NAME;
137    }
138
139    @Override
140    public Map<String, Serializable> getParameters() {
141        checkConfig();
142        return Collections.unmodifiableMap(parameters);
143    }
144
145    @Override
146    public boolean validate(Object value) throws IllegalStateException {
147        checkConfig();
148        if (value != null && value instanceof String) {
149            REF ref = REF.fromValue((String) value);
150            if (ref != null) {
151                try (CoreSession session = CoreInstance.openCoreSession(ref.repo)) {
152                    switch (mode) {
153                    case ID_REF:
154                        return session.exists(new IdRef(ref.ref));
155                    case PATH_REF:
156                        return session.exists(new PathRef(ref.ref));
157                    }
158                } catch (LocalException le) { // no such repo
159                    return false;
160                }
161            }
162        }
163        return false;
164    }
165
166    @Override
167    public Object fetch(Object value) throws IllegalStateException {
168        checkConfig();
169        if (value != null && value instanceof String) {
170            REF ref = REF.fromValue((String) value);
171            if (ref != null) {
172                try (CoreSession session = CoreInstance.openCoreSession(ref.repo)) {
173                    try {
174                        switch (mode) {
175                        case ID_REF:
176                            return session.getDocument(new IdRef(ref.ref));
177                        case PATH_REF:
178                            return session.getDocument(new PathRef(ref.ref));
179                        }
180                    } catch (DocumentNotFoundException e) {
181                        return null;
182                    }
183                } catch (LocalException le) { // no such repo
184                    return null;
185                }
186            }
187        }
188        return null;
189    }
190
191    @Override
192    public <T> T fetch(Class<T> type, Object value) throws IllegalStateException {
193        checkConfig();
194        DocumentModel doc = (DocumentModel) fetch(value);
195        if (doc != null) {
196            if (type.isInstance(doc)) {
197                return type.cast(doc);
198            }
199            return doc.getAdapter(type);
200        }
201        return null;
202    }
203
204    @Override
205    public Serializable getReference(Object entity) throws IllegalStateException {
206        checkConfig();
207        if (entity != null && entity instanceof DocumentModel) {
208            DocumentModel doc = (DocumentModel) entity;
209            String repositoryName = doc.getRepositoryName();
210            if (repositoryName != null) {
211                switch (mode) {
212                case ID_REF:
213                    return repositoryName + ":" + doc.getId();
214                case PATH_REF:
215                    return repositoryName + ":" + doc.getPath().toString();
216                }
217            }
218        }
219        return null;
220    }
221
222    @Override
223    public String getConstraintErrorMessage(Object invalidValue, Locale locale) {
224        checkConfig();
225        switch (mode) {
226        case ID_REF:
227            return Helper.getConstraintErrorMessage(this, "id", invalidValue, locale);
228        case PATH_REF:
229            return Helper.getConstraintErrorMessage(this, "path", invalidValue, locale);
230        default:
231            return String.format("%s cannot resolve reference %s", getName(), invalidValue);
232        }
233    }
234
235    private void checkConfig() throws IllegalStateException {
236        if (parameters == null) {
237            throw new IllegalStateException(
238                    "you should call #configure(Map<String, String>) before. Please get this resolver throught ExternalReferenceService which is in charge of resolver configuration.");
239        }
240    }
241
242    protected static final class REF {
243
244        protected String repo;
245
246        protected String ref;
247
248        protected REF() {
249        }
250
251        protected static REF fromValue(String value) {
252            String[] split = value.split(":");
253            if (split.length == 1) {
254                REF ref = new REF();
255                ref.repo = DEFAULT_REPO_NAME;
256                ref.ref = split[0];
257                return ref;
258            }
259            if (split.length == 2) {
260                REF ref = new REF();
261                ref.repo = split[0];
262                ref.ref = split[1];
263                return ref;
264            }
265            return null;
266        }
267
268    }
269
270}