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