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