001/*
002 * (C) Copyright 2006-2018 Nuxeo (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.util.Arrays;
023import java.util.HashSet;
024import java.util.LinkedList;
025import java.util.List;
026import java.util.Set;
027
028import org.apache.commons.lang3.StringUtils;
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031import org.nuxeo.common.utils.Path;
032import org.nuxeo.ecm.core.api.DocumentModel.DocumentModelRefresh;
033import org.nuxeo.ecm.core.api.impl.DataModelImpl;
034import org.nuxeo.ecm.core.api.impl.DocumentModelImpl;
035import org.nuxeo.ecm.core.api.model.DocumentPart;
036import org.nuxeo.ecm.core.api.model.impl.DocumentPartImpl;
037import org.nuxeo.ecm.core.model.Document;
038import org.nuxeo.ecm.core.model.Document.WriteContext;
039import org.nuxeo.ecm.core.schema.DocumentType;
040import org.nuxeo.ecm.core.schema.FacetNames;
041import org.nuxeo.ecm.core.schema.PrefetchInfo;
042import org.nuxeo.ecm.core.schema.SchemaManager;
043import org.nuxeo.ecm.core.schema.TypeProvider;
044import org.nuxeo.ecm.core.schema.types.Schema;
045import org.nuxeo.runtime.api.Framework;
046
047/**
048 * Bridge between a {@link DocumentModel} and a {@link Document} for creation / update.
049 */
050public class DocumentModelFactory {
051
052    private static final Log log = LogFactory.getLog(DocumentModelFactory.class);
053
054    // Utility class.
055    private DocumentModelFactory() {
056    }
057
058    /**
059     * Creates a document model for an existing document.
060     *
061     * @param doc the document
062     * @param sid the session id for this document
063     * @param schemas the schemas to prefetch (deprecated), or {@code null}
064     * @return the new document model
065     */
066    public static DocumentModelImpl createDocumentModel(Document doc, String sid, String[] schemas) {
067
068        DocumentType type = doc.getType();
069        if (type == null) {
070            throw new NuxeoException("Type not found for doc " + doc);
071        }
072
073        DocumentRef docRef = new IdRef(doc.getUUID());
074        Document parent = doc.getParent();
075        DocumentRef parentRef = parent == null ? null : new IdRef(parent.getUUID());
076
077        // Compute document source id if exists
078        Document sourceDoc = doc.getSourceDocument();
079        String sourceId = sourceDoc == null ? null : sourceDoc.getUUID();
080
081        // Immutable flag
082        boolean immutable = doc.isVersion() || (doc.isProxy() && sourceDoc.isVersion()); // NOSONAR (proxy has source)
083
084        // Instance facets
085        Set<String> facets = new HashSet<>(Arrays.asList(doc.getFacets()));
086        if (immutable) {
087            facets.add(FacetNames.IMMUTABLE);
088        }
089
090        // Compute repository name.
091        String repositoryName = doc.getRepositoryName();
092
093        // versions being imported before their live doc don't have a path
094        String p = doc.getPath();
095        Path path = p == null ? null : new Path(p);
096
097        // create the document model
098        // lock is unused
099        DocumentModelImpl docModel = new DocumentModelImpl(sid, type.getName(), doc.getUUID(), path, docRef, parentRef,
100                null, facets, sourceId, repositoryName, doc.isProxy());
101
102        docModel.setPosInternal(doc.getPos());
103
104        if (doc.isVersion()) {
105            docModel.setIsVersion(true);
106        }
107        if (immutable) {
108            docModel.setIsImmutable(true);
109        }
110
111        // populate datamodels
112        List<String> loadSchemas = new LinkedList<>();
113        if (schemas == null) {
114            PrefetchInfo prefetchInfo = type.getPrefetchInfo();
115            if (prefetchInfo != null) {
116                schemas = prefetchInfo.getSchemas();
117            }
118        }
119        if (schemas != null) {
120            Set<String> validSchemas = new HashSet<>(Arrays.asList(docModel.getSchemas()));
121            for (String schemaName : schemas) {
122                if (validSchemas.contains(schemaName)) {
123                    loadSchemas.add(schemaName);
124                }
125            }
126        }
127        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
128        for (String schemaName : loadSchemas) {
129            Schema schema = schemaManager.getSchema(schemaName);
130            docModel.addDataModel(createDataModel(doc, schema));
131        }
132
133        // prefetch lifecycle state
134        try {
135            String lifeCycleState = doc.getLifeCycleState();
136            docModel.prefetchCurrentLifecycleState(lifeCycleState);
137            String lifeCyclePolicy = doc.getLifeCyclePolicy();
138            docModel.prefetchLifeCyclePolicy(lifeCyclePolicy);
139        } catch (LifeCycleException e) {
140            log.debug("Cannot prefetch lifecycle for doc: " + doc.getName() + ". Error: " + e.getMessage());
141        }
142
143        return docModel;
144    }
145
146    /**
147     * Returns a document model computed from its type, querying the {@link SchemaManager} service.
148     * <p>
149     * The created document model is not linked to any core session.
150     *
151     * @since 5.4.2
152     */
153    public static DocumentModelImpl createDocumentModel(String docType) {
154        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
155        DocumentType type = schemaManager.getDocumentType(docType);
156        return createDocumentModel(null, type);
157    }
158
159    /**
160     * Creates a document model for a new document.
161     * <p>
162     * Initializes the proper data models according to the type info.
163     *
164     * @param sessionId the CoreSession id
165     * @param docType the document type
166     * @return the document model
167     */
168    public static DocumentModelImpl createDocumentModel(String sessionId, DocumentType docType) {
169        DocumentModelImpl docModel = new DocumentModelImpl(sessionId, docType.getName(), null, null, null, null, null,
170                null, null, null, null);
171        for (Schema schema : docType.getSchemas()) {
172            docModel.addDataModel(createDataModel(null, schema));
173        }
174        return docModel;
175    }
176
177    /**
178     * Creates a data model from a document and a schema. If the document is null, just creates empty data models.
179     */
180    public static DataModel createDataModel(Document doc, Schema schema) {
181        DocumentPart part = new DocumentPartImpl(schema);
182        if (doc != null) {
183            doc.readDocumentPart(part);
184        }
185        return new DataModelImpl(part);
186    }
187
188    /**
189     * Writes a document model to a document. Returns the re-read document model.
190     */
191    public static DocumentModel writeDocumentModel(DocumentModel docModel, Document doc) {
192        if (!(docModel instanceof DocumentModelImpl)) {
193            throw new NuxeoException("Must be a DocumentModelImpl: " + docModel);
194        }
195
196        boolean changed = false;
197
198        // change token
199        String changeToken = (String) docModel.getContextData(CoreSession.CHANGE_TOKEN);
200        boolean userChange = StringUtils.isNotEmpty(changeToken);
201        if (!doc.validateUserVisibleChangeToken(changeToken)) {
202            throw new ConcurrentUpdateException(doc.getUUID());
203        }
204        userChange = userChange || Boolean.TRUE.equals(docModel.getContextData(CoreSession.USER_CHANGE));
205        docModel.putContextData(CoreSession.USER_CHANGE, null);
206        if (userChange) {
207            doc.markUserChange();
208        }
209
210        // facets added/removed
211        Set<String> instanceFacets = ((DocumentModelImpl) docModel).instanceFacets;
212        Set<String> instanceFacetsOrig = ((DocumentModelImpl) docModel).instanceFacetsOrig;
213        Set<String> addedFacets = new HashSet<>(instanceFacets);
214        addedFacets.removeAll(instanceFacetsOrig);
215        addedFacets.remove(FacetNames.IMMUTABLE);
216        for (String facet : addedFacets) {
217            changed = doc.addFacet(facet) || changed;
218        }
219        Set<String> removedFacets = new HashSet<>(instanceFacetsOrig);
220        removedFacets.removeAll(instanceFacets);
221        for (String facet : removedFacets) {
222            changed = doc.removeFacet(facet) || changed;
223        }
224
225        // write data models
226        // check only the loaded ones to find the dirty ones
227        WriteContext writeContext = doc.getWriteContext();
228        for (DataModel dm : docModel.getDataModelsCollection()) { // only loaded
229            if (dm.isDirty()) {
230                DocumentPart part = ((DataModelImpl) dm).getDocumentPart();
231                changed = doc.writeDocumentPart(part, writeContext) || changed;
232            }
233        }
234        // write the blobs last, so that blob providers have access to the new doc state
235        writeContext.flush(doc);
236
237        if (!changed) {
238            return docModel;
239        }
240
241        // TODO: here we can optimize document part doesn't need to be read
242        DocumentModel newModel = createDocumentModel(doc, docModel.getSessionId(), null);
243        newModel.copyContextData(docModel);
244        return newModel;
245    }
246
247    /**
248     * Gets what's to refresh in a model (except for the ACPs, which need the session).
249     */
250    public static DocumentModelRefresh refreshDocumentModel(Document doc, int flags, String[] schemas)
251            throws LifeCycleException {
252        DocumentModelRefresh refresh = new DocumentModelRefresh();
253
254        refresh.instanceFacets = new HashSet<>(Arrays.asList(doc.getFacets()));
255        Set<String> docSchemas = DocumentModelImpl.computeSchemas(doc.getType(), refresh.instanceFacets, doc.isProxy());
256
257        if ((flags & DocumentModel.REFRESH_STATE) != 0) {
258            refresh.lifeCycleState = doc.getLifeCycleState();
259            refresh.lifeCyclePolicy = doc.getLifeCyclePolicy();
260            refresh.isCheckedOut = doc.isCheckedOut();
261            refresh.isLatestVersion = doc.isLatestVersion();
262            refresh.isMajorVersion = doc.isMajorVersion();
263            refresh.isLatestMajorVersion = doc.isLatestMajorVersion();
264            refresh.isVersionSeriesCheckedOut = doc.isVersionSeriesCheckedOut();
265            refresh.versionSeriesId = doc.getVersionSeriesId();
266            refresh.checkinComment = doc.getCheckinComment();
267        }
268
269        if ((flags & DocumentModel.REFRESH_CONTENT) != 0) {
270            if (schemas == null) {
271                schemas = docSchemas.toArray(new String[0]);
272            }
273            TypeProvider typeProvider = Framework.getService(SchemaManager.class);
274            DocumentPart[] parts = new DocumentPart[schemas.length];
275            for (int i = 0; i < schemas.length; i++) {
276                DocumentPart part = new DocumentPartImpl(typeProvider.getSchema(schemas[i]));
277                doc.readDocumentPart(part);
278                parts[i] = part;
279            }
280            refresh.documentParts = parts;
281        }
282
283        return refresh;
284    }
285
286    /**
287     * Create an empty documentmodel for a given type with its id already setted. This can be useful when trying to
288     * attach a documentmodel that has been serialized and modified.
289     *
290     * @since 5.7.2
291     */
292    public static DocumentModel createDocumentModel(String type, String id) {
293        SchemaManager sm = Framework.getService(SchemaManager.class);
294        DocumentType docType = sm.getDocumentType(type);
295        DocumentModel doc = new DocumentModelImpl(null, docType.getName(), id, null, null, new IdRef(id), null, null,
296                null, null, null);
297        for (Schema schema : docType.getSchemas()) {
298            ((DocumentModelImpl) doc).addDataModel(createDataModel(null, schema));
299        }
300        return doc;
301    }
302
303}