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