001/*
002 * (C) Copyright 2006-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 *     Bogdan Stefanescu
018 *     Florent Guillaume
019 */
020package org.nuxeo.ecm.core.api;
021
022import java.io.Serializable;
023import java.util.Arrays;
024import java.util.HashSet;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.Map;
028import java.util.Map.Entry;
029import java.util.Set;
030
031import org.apache.commons.lang.StringUtils;
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.nuxeo.common.utils.Path;
035import org.nuxeo.ecm.core.api.DocumentModel.DocumentModelRefresh;
036import org.nuxeo.ecm.core.api.impl.DataModelImpl;
037import org.nuxeo.ecm.core.api.impl.DocumentModelImpl;
038import org.nuxeo.ecm.core.api.model.DocumentPart;
039import org.nuxeo.ecm.core.api.model.impl.DocumentPartImpl;
040import org.nuxeo.ecm.core.model.Document;
041import org.nuxeo.ecm.core.model.Document.WriteContext;
042import org.nuxeo.ecm.core.schema.DocumentType;
043import org.nuxeo.ecm.core.schema.FacetNames;
044import org.nuxeo.ecm.core.schema.Prefetch;
045import org.nuxeo.ecm.core.schema.PrefetchInfo;
046import org.nuxeo.ecm.core.schema.SchemaManager;
047import org.nuxeo.ecm.core.schema.TypeProvider;
048import org.nuxeo.ecm.core.schema.types.Field;
049import org.nuxeo.ecm.core.schema.types.ListType;
050import org.nuxeo.ecm.core.schema.types.Schema;
051import org.nuxeo.ecm.core.schema.types.Type;
052import org.nuxeo.runtime.api.Framework;
053
054/**
055 * Bridge between a {@link DocumentModel} and a {@link Document} for creation / update.
056 */
057public class DocumentModelFactory {
058
059    private static final Log log = LogFactory.getLog(DocumentModelFactory.class);
060
061    // Utility class.
062    private DocumentModelFactory() {
063    }
064
065    /**
066     * Creates a document model for an existing document.
067     *
068     * @param doc the document
069     * @param sid the session id for this document
070     * @param schemas the schemas to prefetch (deprecated), or {@code null}
071     * @return the new document model
072     */
073    public static DocumentModelImpl createDocumentModel(Document doc, String sid, String[] schemas) {
074
075        DocumentType type = doc.getType();
076        if (type == null) {
077            throw new NuxeoException("Type not found for doc " + doc);
078        }
079
080        DocumentRef docRef = new IdRef(doc.getUUID());
081        Document parent = doc.getParent();
082        DocumentRef parentRef = parent == null ? null : new IdRef(parent.getUUID());
083
084        // Compute document source id if exists
085        Document sourceDoc = doc.getSourceDocument();
086        String sourceId = sourceDoc == null ? null : sourceDoc.getUUID();
087
088        // Immutable flag
089        boolean immutable = doc.isVersion() || (doc.isProxy() && sourceDoc.isVersion());
090
091        // Instance facets
092        Set<String> facets = new HashSet<String>(Arrays.asList(doc.getFacets()));
093        if (immutable) {
094            facets.add(FacetNames.IMMUTABLE);
095        }
096
097        // Compute repository name.
098        String repositoryName = doc.getRepositoryName();
099
100        // versions being imported before their live doc don't have a path
101        String p = doc.getPath();
102        Path path = p == null ? null : new Path(p);
103
104        // create the document model
105        // lock is unused
106        DocumentModelImpl docModel = new DocumentModelImpl(sid, type.getName(), doc.getUUID(), path, docRef, parentRef,
107                null, facets, sourceId, repositoryName, doc.isProxy());
108
109        docModel.setPosInternal(doc.getPos());
110
111        if (doc.isVersion()) {
112            docModel.setIsVersion(true);
113        }
114        if (immutable) {
115            docModel.setIsImmutable(true);
116        }
117
118        // populate prefetch
119        PrefetchInfo prefetchInfo = type.getPrefetchInfo();
120        Prefetch prefetch;
121        String[] prefetchSchemas;
122        if (prefetchInfo != null) {
123            Set<String> docSchemas = new HashSet<String>(Arrays.asList(docModel.getSchemas()));
124            prefetch = getPrefetch(doc, prefetchInfo, docSchemas);
125            prefetchSchemas = prefetchInfo.getSchemas();
126        } else {
127            prefetch = null;
128            prefetchSchemas = null;
129        }
130
131        // populate datamodels
132        List<String> loadSchemas = new LinkedList<String>();
133        if (schemas == null) {
134            schemas = prefetchSchemas;
135        }
136        if (schemas != null) {
137            Set<String> validSchemas = new HashSet<String>(Arrays.asList(docModel.getSchemas()));
138            for (String schemaName : schemas) {
139                if (validSchemas.contains(schemaName)) {
140                    loadSchemas.add(schemaName);
141                }
142            }
143        }
144        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
145        for (String schemaName : loadSchemas) {
146            Schema schema = schemaManager.getSchema(schemaName);
147            docModel.addDataModel(createDataModel(doc, schema));
148        }
149
150        if (prefetch != null) {
151            // ignore prefetches already loaded as datamodels
152            for (String schemaName : loadSchemas) {
153                prefetch.clearPrefetch(schemaName);
154            }
155            // set prefetch
156            docModel.setPrefetch(prefetch);
157        }
158
159        // prefetch lifecycle state
160        try {
161            String lifeCycleState = doc.getLifeCycleState();
162            docModel.prefetchCurrentLifecycleState(lifeCycleState);
163            String lifeCyclePolicy = doc.getLifeCyclePolicy();
164            docModel.prefetchLifeCyclePolicy(lifeCyclePolicy);
165        } catch (LifeCycleException e) {
166            log.debug("Cannot prefetch lifecycle for doc: " + doc.getName() + ". Error: " + e.getMessage());
167        }
168
169        return docModel;
170    }
171
172    /**
173     * Returns a document model computed from its type, querying the {@link SchemaManager} service.
174     * <p>
175     * The created document model is not linked to any core session.
176     *
177     * @since 5.4.2
178     */
179    public static DocumentModelImpl createDocumentModel(String docType) {
180        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
181        DocumentType type = schemaManager.getDocumentType(docType);
182        return createDocumentModel(null, type);
183    }
184
185    /**
186     * Creates a document model for a new document.
187     * <p>
188     * Initializes the proper data models according to the type info.
189     *
190     * @param sessionId the CoreSession id
191     * @param docType the document type
192     * @return the document model
193     */
194    public static DocumentModelImpl createDocumentModel(String sessionId, DocumentType docType) {
195        DocumentModelImpl docModel = new DocumentModelImpl(sessionId, docType.getName(), null, null, null, null, null,
196                null, null, null, null);
197        for (Schema schema : docType.getSchemas()) {
198            docModel.addDataModel(createDataModel(null, schema));
199        }
200        return docModel;
201    }
202
203    /**
204     * Creates a data model from a document and a schema. If the document is null, just creates empty data models.
205     */
206    public static DataModel createDataModel(Document doc, Schema schema) {
207        DocumentPart part = new DocumentPartImpl(schema);
208        if (doc != null) {
209            doc.readDocumentPart(part);
210        }
211        return new DataModelImpl(part);
212    }
213
214    /**
215     * Writes a document model to a document. Returns the re-read document model.
216     */
217    public static DocumentModel writeDocumentModel(DocumentModel docModel, Document doc) {
218        if (!(docModel instanceof DocumentModelImpl)) {
219            throw new NuxeoException("Must be a DocumentModelImpl: " + docModel);
220        }
221
222        boolean changed = false;
223
224        // change token
225        String changeToken = (String) docModel.getContextData(CoreSession.CHANGE_TOKEN);
226        boolean userChange = StringUtils.isNotEmpty(changeToken);
227        if (!doc.validateUserVisibleChangeToken(changeToken)) {
228            throw new ConcurrentUpdateException(doc.getUUID());
229        }
230        userChange = userChange || Boolean.TRUE.equals(docModel.getContextData(CoreSession.USER_CHANGE));
231        docModel.putContextData(CoreSession.USER_CHANGE, null);
232        if (userChange) {
233            doc.markUserChange();
234        }
235
236        // facets added/removed
237        Set<String> instanceFacets = ((DocumentModelImpl) docModel).instanceFacets;
238        Set<String> instanceFacetsOrig = ((DocumentModelImpl) docModel).instanceFacetsOrig;
239        Set<String> addedFacets = new HashSet<String>(instanceFacets);
240        addedFacets.removeAll(instanceFacetsOrig);
241        for (String facet : addedFacets) {
242            changed = doc.addFacet(facet) || changed;
243        }
244        Set<String> removedFacets = new HashSet<String>(instanceFacetsOrig);
245        removedFacets.removeAll(instanceFacets);
246        for (String facet : removedFacets) {
247            changed = doc.removeFacet(facet) || changed;
248        }
249
250        // write data models
251        // check only the loaded ones to find the dirty ones
252        WriteContext writeContext = doc.getWriteContext();
253        for (DataModel dm : docModel.getDataModelsCollection()) { // only loaded
254            if (dm.isDirty()) {
255                DocumentPart part = ((DataModelImpl) dm).getDocumentPart();
256                changed = doc.writeDocumentPart(part, writeContext) || changed;
257            }
258        }
259        // write the blobs last, so that blob providers have access to the new doc state
260        writeContext.flush(doc);
261
262        if (!changed) {
263            return docModel;
264        }
265
266        // TODO: here we can optimize document part doesn't need to be read
267        DocumentModel newModel = createDocumentModel(doc, docModel.getSessionId(), null);
268        newModel.copyContextData(docModel);
269        return newModel;
270    }
271
272    /**
273     * Gets what's to refresh in a model (except for the ACPs, which need the session).
274     */
275    public static DocumentModelRefresh refreshDocumentModel(Document doc, int flags, String[] schemas)
276            throws LifeCycleException {
277        DocumentModelRefresh refresh = new DocumentModelRefresh();
278
279        refresh.instanceFacets = new HashSet<String>(Arrays.asList(doc.getFacets()));
280        Set<String> docSchemas = DocumentModelImpl.computeSchemas(doc.getType(), refresh.instanceFacets, doc.isProxy());
281
282        if ((flags & DocumentModel.REFRESH_PREFETCH) != 0) {
283            PrefetchInfo prefetchInfo = doc.getType().getPrefetchInfo();
284            if (prefetchInfo != null) {
285                refresh.prefetch = getPrefetch(doc, prefetchInfo, docSchemas);
286            }
287        }
288
289        if ((flags & DocumentModel.REFRESH_STATE) != 0) {
290            refresh.lifeCycleState = doc.getLifeCycleState();
291            refresh.lifeCyclePolicy = doc.getLifeCyclePolicy();
292            refresh.isCheckedOut = doc.isCheckedOut();
293            refresh.isLatestVersion = doc.isLatestVersion();
294            refresh.isMajorVersion = doc.isMajorVersion();
295            refresh.isLatestMajorVersion = doc.isLatestMajorVersion();
296            refresh.isVersionSeriesCheckedOut = doc.isVersionSeriesCheckedOut();
297            refresh.versionSeriesId = doc.getVersionSeriesId();
298            refresh.checkinComment = doc.getCheckinComment();
299        }
300
301        if ((flags & DocumentModel.REFRESH_CONTENT) != 0) {
302            if (schemas == null) {
303                schemas = docSchemas.toArray(new String[0]);
304            }
305            TypeProvider typeProvider = Framework.getLocalService(SchemaManager.class);
306            DocumentPart[] parts = new DocumentPart[schemas.length];
307            for (int i = 0; i < schemas.length; i++) {
308                DocumentPart part = new DocumentPartImpl(typeProvider.getSchema(schemas[i]));
309                doc.readDocumentPart(part);
310                parts[i] = part;
311            }
312            refresh.documentParts = parts;
313        }
314
315        return refresh;
316    }
317
318    /**
319     * Prefetches from a document.
320     */
321    protected static Prefetch getPrefetch(Document doc, PrefetchInfo prefetchInfo, Set<String> docSchemas) {
322        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
323
324        // individual fields
325        Set<String> xpaths = new HashSet<String>();
326        String[] prefetchFields = prefetchInfo.getFields();
327        if (prefetchFields != null) {
328            xpaths.addAll(Arrays.asList(prefetchFields));
329        }
330
331        // whole schemas (but NOT their complex properties)
332        String[] prefetchSchemas = prefetchInfo.getSchemas();
333        if (prefetchSchemas != null) {
334            for (String schemaName : prefetchSchemas) {
335                if (docSchemas.contains(schemaName)) {
336                    Schema schema = schemaManager.getSchema(schemaName);
337                    if (schema != null) {
338                        for (Field field : schema.getFields()) {
339                            if (isScalarField(field)) {
340                                xpaths.add(field.getName().getPrefixedName());
341                            }
342                        }
343                    }
344                }
345            }
346        }
347
348        // do the prefetch
349        Prefetch prefetch = new Prefetch();
350        for (String schemaName : docSchemas) {
351            Schema schema = schemaManager.getSchema(schemaName);
352            // find xpaths for this schema
353            Set<String> schemaXpaths = new HashSet<String>();
354            for (String xpath : xpaths) {
355                String sn = DocumentModelImpl.getXPathSchemaName(xpath, docSchemas, null);
356                if (schemaName.equals(sn)) {
357                    schemaXpaths.add(xpath);
358                }
359            }
360            if (schemaXpaths.isEmpty()) {
361                continue;
362            }
363            Map<String, Serializable> map = doc.readPrefetch(schema, schemaXpaths);
364            for (Entry<String, Serializable> en : map.entrySet()) {
365                String xpath = en.getKey();
366                Serializable value = en.getValue();
367                String[] returnName = new String[1];
368                String sn = DocumentModelImpl.getXPathSchemaName(xpath, docSchemas, returnName);
369                String name = returnName[0];
370                prefetch.put(xpath, sn, name, value);
371            }
372        }
373
374        return prefetch;
375    }
376
377    /**
378     * Checks if a field is a primitive type or array.
379     */
380    protected static boolean isScalarField(Field field) {
381        Type type = field.getType();
382        if (type.isComplexType()) {
383            // complex type
384            return false;
385        }
386        if (!type.isListType()) {
387            // primitive type
388            return true;
389        }
390        // array or complex list?
391        return ((ListType) type).getFieldType().isSimpleType();
392    }
393
394    /**
395     * Create an empty documentmodel for a given type with its id already setted. This can be useful when trying to
396     * attach a documentmodel that has been serialized and modified.
397     *
398     * @param type
399     * @param id
400     * @return
401     * @since 5.7.2
402     */
403    public static DocumentModel createDocumentModel(String type, String id) {
404        SchemaManager sm = Framework.getLocalService(SchemaManager.class);
405        DocumentType docType = sm.getDocumentType(type);
406        DocumentModel doc = new DocumentModelImpl(null, docType.getName(), id, null, null, new IdRef(id), null, null,
407                null, null, null);
408        for (Schema schema : docType.getSchemas()) {
409            ((DocumentModelImpl) doc).addDataModel(createDataModel(null, schema));
410        }
411        return doc;
412    }
413
414}