001/*
002 * (C) Copyright 2006-2019 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     * @return the new document model
063     */
064    public static DocumentModelImpl createDocumentModel(Document doc, CoreSession coreSession) {
065
066        DocumentType type = doc.getType();
067        if (type == null) {
068            throw new NuxeoException("Type not found for doc " + doc);
069        }
070
071        DocumentRef docRef = new IdRef(doc.getUUID());
072        Document parent = doc.getParent();
073        DocumentRef parentRef = parent == null ? null : new IdRef(parent.getUUID());
074
075        // Compute document source id if exists
076        Document sourceDoc = doc.getSourceDocument();
077        String sourceId = sourceDoc == null ? null : sourceDoc.getUUID();
078
079        // Immutable flag
080        boolean immutable = doc.isVersion() || (doc.isProxy() && sourceDoc.isVersion()); // NOSONAR (proxy has source)
081
082        // Instance facets
083        Set<String> facets = new HashSet<>(Arrays.asList(doc.getFacets()));
084        if (immutable) {
085            facets.add(FacetNames.IMMUTABLE);
086        }
087
088        // Compute repository name.
089        String repositoryName = doc.getRepositoryName();
090
091        // versions being imported before their live doc don't have a path
092        String p = doc.getPath();
093        Path path = p == null ? null : new Path(p);
094
095        // create the document model
096        DocumentModelImpl docModel = new DocumentModelImpl(type.getName(), doc.getUUID(), path, docRef, parentRef, null,
097                facets, sourceId, doc.isProxy(), coreSession, repositoryName, null);
098
099        docModel.setPosInternal(doc.getPos());
100
101        if (doc.isVersion()) {
102            docModel.setIsVersion(true);
103        }
104        if (immutable) {
105            docModel.setIsImmutable(true);
106        }
107
108        // populate datamodels
109        List<String> loadSchemas = new LinkedList<>();
110        PrefetchInfo prefetchInfo = type.getPrefetchInfo();
111        if (prefetchInfo != null) {
112            String[] schemas = prefetchInfo.getSchemas();
113            if (schemas != null) {
114                Set<String> validSchemas = new HashSet<>(Arrays.asList(docModel.getSchemas()));
115                for (String schemaName : schemas) {
116                    if (validSchemas.contains(schemaName)) {
117                        loadSchemas.add(schemaName);
118                    }
119                }
120            }
121        }
122        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
123        for (String schemaName : loadSchemas) {
124            Schema schema = schemaManager.getSchema(schemaName);
125            docModel.addDataModel(createDataModel(doc, schema));
126        }
127
128        // prefetch lifecycle state
129        try {
130            String lifeCycleState = doc.getLifeCycleState();
131            docModel.prefetchCurrentLifecycleState(lifeCycleState);
132            String lifeCyclePolicy = doc.getLifeCyclePolicy();
133            docModel.prefetchLifeCyclePolicy(lifeCyclePolicy);
134        } catch (LifeCycleException e) {
135            log.debug("Cannot prefetch lifecycle for doc: " + doc.getName() + ". Error: " + e.getMessage());
136        }
137
138        return docModel;
139    }
140
141    /**
142     * Returns a document model computed from its type, querying the {@link SchemaManager} service.
143     * <p>
144     * The created document model is not linked to any core session.
145     *
146     * @since 5.4.2
147     */
148    public static DocumentModelImpl createDocumentModel(String docType) {
149        return createDocumentModel(docType, null, null);
150    }
151
152    /**
153     * Creates a document model for a new document.
154     * <p>
155     * Initializes the proper data models according to the type info.
156     *
157     * @param typeName the document type
158     * @param parentRef the parent ref
159     * @return the document model
160     * @since 11.1
161     */
162    public static DocumentModelImpl createDocumentModel(String typeName, DocumentRef parentRef,
163            CoreSession coreSession) {
164        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
165        DocumentType type = schemaManager.getDocumentType(typeName);
166        if (type == null) {
167            throw new IllegalArgumentException(typeName + " is not a registered core type");
168        }
169        DocumentModelImpl docModel = new DocumentModelImpl(typeName, null, null, null, parentRef, null, null, null,
170                false, coreSession, null, null);
171        for (Schema schema : type.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    @SuppressWarnings("deprecation")
181    public static DataModel createDataModel(Document doc, Schema schema) {
182        DocumentPart part = new DocumentPartImpl(schema);
183        if (doc != null) {
184            doc.readDocumentPart(part);
185        }
186        return new DataModelImpl(part);
187    }
188
189    /**
190     * Writes a document model to a document. Returns the re-read document model.
191     */
192    public static DocumentModel writeDocumentModel(DocumentModel docModel, Document doc) {
193        return writeDocumentModel(docModel, doc, false);
194    }
195
196    /**
197     * Writes a document model to a document. Returns the re-read document model.
198     *
199     * @since 11.1
200     */
201    @SuppressWarnings("deprecation")
202    public static DocumentModel writeDocumentModel(DocumentModel docModel, Document doc, boolean create) {
203        if (!(docModel instanceof DocumentModelImpl)) {
204            throw new NuxeoException("Must be a DocumentModelImpl: " + docModel);
205        }
206
207        boolean changed = false;
208
209        // change token
210        String changeToken = (String) docModel.getContextData(CoreSession.CHANGE_TOKEN);
211        boolean userChange = StringUtils.isNotEmpty(changeToken);
212        if (!doc.validateUserVisibleChangeToken(changeToken)) {
213            throw new ConcurrentUpdateException("Invalid change token").addInfo(doc.getUUID());
214        }
215        userChange = userChange || Boolean.TRUE.equals(docModel.getContextData(CoreSession.USER_CHANGE));
216        docModel.putContextData(CoreSession.USER_CHANGE, null);
217        if (userChange) {
218            doc.markUserChange();
219        }
220
221        // facets added/removed
222        Set<String> instanceFacets = ((DocumentModelImpl) docModel).instanceFacets;
223        Set<String> instanceFacetsOrig = ((DocumentModelImpl) docModel).instanceFacetsOrig;
224        Set<String> addedFacets = new HashSet<>(instanceFacets);
225        addedFacets.removeAll(instanceFacetsOrig);
226        addedFacets.remove(FacetNames.IMMUTABLE);
227        for (String facet : addedFacets) {
228            changed = doc.addFacet(facet) || changed;
229        }
230        Set<String> removedFacets = new HashSet<>(instanceFacetsOrig);
231        removedFacets.removeAll(instanceFacets);
232        for (String facet : removedFacets) {
233            changed = doc.removeFacet(facet) || changed;
234        }
235
236        // write data models
237        // check only the loaded ones to find the dirty ones
238        WriteContext writeContext = doc.getWriteContext();
239        for (DataModel dm : docModel.getDataModelsCollection()) { // only loaded
240            DocumentPart part = ((DataModelImpl) dm).getDocumentPart();
241            // check if we have dirty properties or default values to write
242            if (create || dm.isDirty() || part.isPhantom() && part.hasDefaultValue()) {
243                changed |= doc.writeDocumentPart(part, writeContext, create);
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.getCoreSession());
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<>(Arrays.asList(doc.getFacets()));
267        Set<String> docSchemas = DocumentModelImpl.computeSchemas(doc.getType(), refresh.instanceFacets, doc.isProxy());
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            refresh.isRecord = doc.isRecord();
280            refresh.retainUntil = doc.getRetainUntil();
281            refresh.hasLegalHold = doc.hasLegalHold();
282        }
283
284        if ((flags & DocumentModel.REFRESH_CONTENT) != 0) {
285            if (schemas == null) {
286                schemas = docSchemas.toArray(new String[0]);
287            }
288            TypeProvider typeProvider = Framework.getService(SchemaManager.class);
289            @SuppressWarnings("deprecation")
290            DocumentPart[] parts = new DocumentPart[schemas.length];
291            for (int i = 0; i < schemas.length; i++) {
292                @SuppressWarnings("deprecation")
293                DocumentPart part = new DocumentPartImpl(typeProvider.getSchema(schemas[i]));
294                doc.readDocumentPart(part);
295                parts[i] = part;
296            }
297            refresh.documentParts = parts;
298        }
299
300        return refresh;
301    }
302
303    /**
304     * Create an empty documentmodel for a given type with its id already setted. This can be useful when trying to
305     * attach a documentmodel that has been serialized and modified.
306     *
307     * @since 5.7.2
308     * @deprecated since 11.1, unused
309     */
310    @Deprecated
311    public static DocumentModel createDocumentModel(String type, String id) {
312        SchemaManager sm = Framework.getService(SchemaManager.class);
313        DocumentType docType = sm.getDocumentType(type);
314        DocumentModelImpl doc = new DocumentModelImpl(type, id, null, new IdRef(id), null, null, null, null, false,
315                null, null, null);
316        docType.getSchemas().forEach(schema -> doc.addDataModel(createDataModel(null, schema)));
317        return doc;
318    }
319
320}