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