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}