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        // facets added/removed
224        Set<String> instanceFacets = ((DocumentModelImpl) docModel).instanceFacets;
225        Set<String> instanceFacetsOrig = ((DocumentModelImpl) docModel).instanceFacetsOrig;
226        Set<String> addedFacets = new HashSet<String>(instanceFacets);
227        addedFacets.removeAll(instanceFacetsOrig);
228        for (String facet : addedFacets) {
229            changed = doc.addFacet(facet) || changed;
230        }
231        Set<String> removedFacets = new HashSet<String>(instanceFacetsOrig);
232        removedFacets.removeAll(instanceFacets);
233        for (String facet : removedFacets) {
234            changed = doc.removeFacet(facet) || changed;
235        }
236
237        // write data models
238        // check only the loaded ones to find the dirty ones
239        WriteContext writeContext = doc.getWriteContext();
240        for (DataModel dm : docModel.getDataModelsCollection()) { // only loaded
241            if (dm.isDirty()) {
242                DocumentPart part = ((DataModelImpl) dm).getDocumentPart();
243                changed = doc.writeDocumentPart(part, writeContext) || changed;
244            }
245        }
246        // write the blobs last, so that blob providers have access to the new doc state
247        writeContext.flush(doc);
248
249        if (!changed) {
250            return docModel;
251        }
252
253        // TODO: here we can optimize document part doesn't need to be read
254        DocumentModel newModel = createDocumentModel(doc, docModel.getSessionId(), null);
255        newModel.copyContextData(docModel);
256        return newModel;
257    }
258
259    /**
260     * Gets what's to refresh in a model (except for the ACPs, which need the session).
261     */
262    public static DocumentModelRefresh refreshDocumentModel(Document doc, int flags, String[] schemas)
263            throws LifeCycleException {
264        DocumentModelRefresh refresh = new DocumentModelRefresh();
265
266        refresh.instanceFacets = new HashSet<String>(Arrays.asList(doc.getFacets()));
267        Set<String> docSchemas = DocumentModelImpl.computeSchemas(doc.getType(), refresh.instanceFacets, doc.isProxy());
268
269        if ((flags & DocumentModel.REFRESH_PREFETCH) != 0) {
270            PrefetchInfo prefetchInfo = doc.getType().getPrefetchInfo();
271            if (prefetchInfo != null) {
272                refresh.prefetch = getPrefetch(doc, prefetchInfo, docSchemas);
273            }
274        }
275
276        if ((flags & DocumentModel.REFRESH_STATE) != 0) {
277            refresh.lifeCycleState = doc.getLifeCycleState();
278            refresh.lifeCyclePolicy = doc.getLifeCyclePolicy();
279            refresh.isCheckedOut = doc.isCheckedOut();
280            refresh.isLatestVersion = doc.isLatestVersion();
281            refresh.isMajorVersion = doc.isMajorVersion();
282            refresh.isLatestMajorVersion = doc.isLatestMajorVersion();
283            refresh.isVersionSeriesCheckedOut = doc.isVersionSeriesCheckedOut();
284            refresh.versionSeriesId = doc.getVersionSeriesId();
285            refresh.checkinComment = doc.getCheckinComment();
286        }
287
288        if ((flags & DocumentModel.REFRESH_CONTENT) != 0) {
289            if (schemas == null) {
290                schemas = docSchemas.toArray(new String[0]);
291            }
292            TypeProvider typeProvider = Framework.getLocalService(SchemaManager.class);
293            DocumentPart[] parts = new DocumentPart[schemas.length];
294            for (int i = 0; i < schemas.length; i++) {
295                DocumentPart part = new DocumentPartImpl(typeProvider.getSchema(schemas[i]));
296                doc.readDocumentPart(part);
297                parts[i] = part;
298            }
299            refresh.documentParts = parts;
300        }
301
302        return refresh;
303    }
304
305    /**
306     * Prefetches from a document.
307     */
308    protected static Prefetch getPrefetch(Document doc, PrefetchInfo prefetchInfo, Set<String> docSchemas) {
309        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
310
311        // individual fields
312        Set<String> xpaths = new HashSet<String>();
313        String[] prefetchFields = prefetchInfo.getFields();
314        if (prefetchFields != null) {
315            xpaths.addAll(Arrays.asList(prefetchFields));
316        }
317
318        // whole schemas (but NOT their complex properties)
319        String[] prefetchSchemas = prefetchInfo.getSchemas();
320        if (prefetchSchemas != null) {
321            for (String schemaName : prefetchSchemas) {
322                if (docSchemas.contains(schemaName)) {
323                    Schema schema = schemaManager.getSchema(schemaName);
324                    if (schema != null) {
325                        for (Field field : schema.getFields()) {
326                            if (isScalarField(field)) {
327                                xpaths.add(field.getName().getPrefixedName());
328                            }
329                        }
330                    }
331                }
332            }
333        }
334
335        // do the prefetch
336        Prefetch prefetch = new Prefetch();
337        for (String schemaName : docSchemas) {
338            Schema schema = schemaManager.getSchema(schemaName);
339            // find xpaths for this schema
340            Set<String> schemaXpaths = new HashSet<String>();
341            for (String xpath : xpaths) {
342                String sn = DocumentModelImpl.getXPathSchemaName(xpath, docSchemas, null);
343                if (schemaName.equals(sn)) {
344                    schemaXpaths.add(xpath);
345                }
346            }
347            if (schemaXpaths.isEmpty()) {
348                continue;
349            }
350            Map<String, Serializable> map = doc.readPrefetch(schema, schemaXpaths);
351            for (Entry<String, Serializable> en : map.entrySet()) {
352                String xpath = en.getKey();
353                Serializable value = en.getValue();
354                String[] returnName = new String[1];
355                String sn = DocumentModelImpl.getXPathSchemaName(xpath, docSchemas, returnName);
356                String name = returnName[0];
357                prefetch.put(xpath, sn, name, value);
358            }
359        }
360
361        return prefetch;
362    }
363
364    /**
365     * Checks if a field is a primitive type or array.
366     */
367    protected static boolean isScalarField(Field field) {
368        Type type = field.getType();
369        if (type.isComplexType()) {
370            // complex type
371            return false;
372        }
373        if (!type.isListType()) {
374            // primitive type
375            return true;
376        }
377        // array or complex list?
378        return ((ListType) type).getFieldType().isSimpleType();
379    }
380
381    /**
382     * Create an empty documentmodel for a given type with its id already setted. This can be useful when trying to
383     * attach a documentmodel that has been serialized and modified.
384     *
385     * @param type
386     * @param id
387     * @return
388     * @since 5.7.2
389     */
390    public static DocumentModel createDocumentModel(String type, String id) {
391        SchemaManager sm = Framework.getLocalService(SchemaManager.class);
392        DocumentType docType = sm.getDocumentType(type);
393        DocumentModel doc = new DocumentModelImpl(null, docType.getName(), id, null, null, new IdRef(id), null, null,
394                null, null, null);
395        for (Schema schema : docType.getSchemas()) {
396            ((DocumentModelImpl) doc).addDataModel(createDataModel(null, schema));
397        }
398        return doc;
399    }
400
401}