001/* 002 * (C) Copyright 2006-2016 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 * bstefanescu 018 */ 019package org.nuxeo.ecm.core.io.impl; 020 021import java.lang.reflect.Array; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.Calendar; 025import java.util.Date; 026import java.util.GregorianCalendar; 027import java.util.HashMap; 028import java.util.Iterator; 029import java.util.List; 030import java.util.Map; 031 032import org.apache.commons.codec.binary.Base64; 033import org.apache.commons.logging.Log; 034import org.apache.commons.logging.LogFactory; 035import org.dom4j.Document; 036import org.dom4j.Element; 037import org.nuxeo.common.collections.PrimitiveArrays; 038import org.nuxeo.common.utils.Path; 039import org.nuxeo.ecm.core.api.Blob; 040import org.nuxeo.ecm.core.api.Blobs; 041import org.nuxeo.ecm.core.api.CoreSession; 042import org.nuxeo.ecm.core.api.DocumentLocation; 043import org.nuxeo.ecm.core.api.DocumentModel; 044import org.nuxeo.ecm.core.api.NuxeoException; 045import org.nuxeo.ecm.core.api.security.ACE; 046import org.nuxeo.ecm.core.api.security.ACL; 047import org.nuxeo.ecm.core.api.security.ACP; 048import org.nuxeo.ecm.core.api.security.impl.ACLImpl; 049import org.nuxeo.ecm.core.api.security.impl.ACPImpl; 050import org.nuxeo.ecm.core.api.versioning.VersioningService; 051import org.nuxeo.ecm.core.io.ExportConstants; 052import org.nuxeo.ecm.core.io.ExportedDocument; 053import org.nuxeo.ecm.core.schema.SchemaManager; 054import org.nuxeo.ecm.core.schema.TypeConstants; 055import org.nuxeo.ecm.core.schema.types.ComplexType; 056import org.nuxeo.ecm.core.schema.types.CompositeType; 057import org.nuxeo.ecm.core.schema.types.Field; 058import org.nuxeo.ecm.core.schema.types.JavaTypes; 059import org.nuxeo.ecm.core.schema.types.ListType; 060import org.nuxeo.ecm.core.schema.types.Schema; 061import org.nuxeo.ecm.core.schema.types.Type; 062import org.nuxeo.ecm.core.schema.utils.DateParser; 063import org.nuxeo.runtime.api.Framework; 064 065/** 066 * @author <a href="mailto:[email protected]">Bogdan Stefanescu</a> 067 */ 068// TODO: improve it -> 069// modify core session to add a batch create method and use it 070public abstract class AbstractDocumentModelWriter extends AbstractDocumentWriter { 071 072 private static final Log log = LogFactory.getLog(AbstractDocumentModelWriter.class); 073 074 protected CoreSession session; 075 076 protected Path root; 077 078 private int saveInterval; 079 080 protected int unsavedDocuments = 0; 081 082 private final Map<DocumentLocation, DocumentLocation> translationMap = new HashMap<DocumentLocation, DocumentLocation>(); 083 084 /** 085 * @param session the session to the repository where to write 086 * @param parentPath where to write the tree. this document will be used as the parent of all top level documents 087 * passed as input. Note that you may have 088 */ 089 protected AbstractDocumentModelWriter(CoreSession session, String parentPath) { 090 this(session, parentPath, 10); 091 } 092 093 protected AbstractDocumentModelWriter(CoreSession session, String parentPath, int saveInterval) { 094 if (session == null) { 095 throw new IllegalArgumentException("null session"); 096 } 097 this.session = session; 098 this.saveInterval = saveInterval; 099 root = new Path(parentPath); 100 } 101 102 public Map<DocumentLocation, DocumentLocation> getTranslationMap() { 103 return translationMap; 104 } 105 106 protected void saveIfNeeded() { 107 if (unsavedDocuments >= saveInterval) { 108 session.save(); 109 unsavedDocuments = 0; 110 } 111 } 112 113 @Override 114 public void close() { 115 if (unsavedDocuments > 0) { 116 session.save(); 117 } 118 session = null; 119 root = null; 120 } 121 122 /** 123 * Creates a new document given its path. 124 * <p> 125 * The parent of this document is assumed to exist. 126 * 127 * @param xdoc the document containing 128 * @param toPath the path of the doc to create 129 */ 130 protected DocumentModel createDocument(ExportedDocument xdoc, Path toPath) { 131 Path parentPath = toPath.removeLastSegments(1); 132 String name = toPath.lastSegment(); 133 134 DocumentModel doc = session.createDocumentModel(parentPath.toString(), name, xdoc.getType()); 135 136 // set lifecycle state at creation 137 Element system = xdoc.getDocument().getRootElement().element(ExportConstants.SYSTEM_TAG); 138 String lifeCycleState = system.element(ExportConstants.LIFECYCLE_STATE_TAG).getText(); 139 doc.putContextData("initialLifecycleState", lifeCycleState); 140 141 // loadFacets before schemas so that additional schemas are not skipped 142 loadFacetsInfo(doc, xdoc.getDocument()); 143 144 // then load schemas data 145 loadSchemas(xdoc, doc, xdoc.getDocument()); 146 147 if (doc.hasSchema("uid")) { 148 doc.putContextData(VersioningService.SKIP_VERSIONING, true); 149 } 150 151 beforeCreateDocument(doc); 152 doc = session.createDocument(doc); 153 154 // load into the document the system properties, document needs to exist 155 loadSystemInfo(doc, xdoc.getDocument()); 156 157 unsavedDocuments += 1; 158 saveIfNeeded(); 159 160 return doc; 161 } 162 163 /** 164 * @since 8.4 165 */ 166 protected void beforeCreateDocument(DocumentModel doc) { 167 // Empty default implementation 168 } 169 170 /** 171 * Updates an existing document. 172 */ 173 protected DocumentModel updateDocument(ExportedDocument xdoc, DocumentModel doc) { 174 // load schemas data 175 loadSchemas(xdoc, doc, xdoc.getDocument()); 176 177 loadFacetsInfo(doc, xdoc.getDocument()); 178 179 beforeSaveDocument(doc); 180 doc = session.saveDocument(doc); 181 182 unsavedDocuments += 1; 183 saveIfNeeded(); 184 185 return doc; 186 } 187 188 /** 189 * @since 8.4 190 */ 191 protected void beforeSaveDocument(DocumentModel doc) { 192 // Empty default implementation 193 } 194 195 public int getSaveInterval() { 196 return saveInterval; 197 } 198 199 public void setSaveInterval(int saveInterval) { 200 this.saveInterval = saveInterval; 201 } 202 203 @SuppressWarnings("unchecked") 204 protected boolean loadFacetsInfo(DocumentModel docModel, Document doc) { 205 boolean added = false; 206 Element system = doc.getRootElement().element(ExportConstants.SYSTEM_TAG); 207 if (system == null) { 208 return false; 209 } 210 211 Iterator<Element> facets = system.elementIterator(ExportConstants.FACET_TAG); 212 while (facets.hasNext()) { 213 Element element = facets.next(); 214 String facet = element.getTextTrim(); 215 216 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 217 CompositeType facetType = schemaManager.getFacet(facet); 218 219 if (facetType == null) { 220 log.warn("The document " + docModel.getName() + " with id=" + docModel.getId() + " and type=" 221 + docModel.getDocumentType().getName() + " contains the facet '" + facet 222 + "', which is not registered as available in the schemaManager. This facet will be ignored."); 223 if (log.isDebugEnabled()) { 224 log.debug("Available facets: " + Arrays.toString(schemaManager.getFacets())); 225 } 226 continue; 227 } 228 229 if (!docModel.hasFacet(facet)) { 230 docModel.addFacet(facet); 231 added = true; 232 } 233 } 234 235 return added; 236 } 237 238 @SuppressWarnings("unchecked") 239 protected void loadSystemInfo(DocumentModel docModel, Document doc) { 240 Element system = doc.getRootElement().element(ExportConstants.SYSTEM_TAG); 241 242 Element accessControl = system.element(ExportConstants.ACCESS_CONTROL_TAG); 243 if (accessControl == null) { 244 return; 245 } 246 Iterator<Element> it = accessControl.elementIterator(ExportConstants.ACL_TAG); 247 while (it.hasNext()) { 248 Element element = it.next(); 249 // import only the local acl 250 if (ACL.LOCAL_ACL.equals(element.attributeValue(ExportConstants.NAME_ATTR))) { 251 // this is the local ACL - import it 252 List<Element> entries = element.elements(); 253 int size = entries.size(); 254 if (size > 0) { 255 ACP acp = new ACPImpl(); 256 ACL acl = new ACLImpl(ACL.LOCAL_ACL); 257 acp.addACL(acl); 258 for (Element el : entries) { 259 String username = el.attributeValue(ExportConstants.PRINCIPAL_ATTR); 260 String permission = el.attributeValue(ExportConstants.PERMISSION_ATTR); 261 String grant = el.attributeValue(ExportConstants.GRANT_ATTR); 262 String creator = el.attributeValue(ExportConstants.CREATOR_ATTR); 263 String beginStr = el.attributeValue(ExportConstants.BEGIN_ATTR); 264 Calendar begin = null; 265 if (beginStr != null) { 266 Date date = DateParser.parseW3CDateTime(beginStr); 267 begin = new GregorianCalendar(); 268 begin.setTimeInMillis(date.getTime()); 269 } 270 String endStr = el.attributeValue(ExportConstants.END_ATTR); 271 Calendar end = null; 272 if (endStr != null) { 273 Date date = DateParser.parseW3CDateTime(endStr); 274 end = new GregorianCalendar(); 275 end.setTimeInMillis(date.getTime()); 276 } 277 ACE ace = ACE.builder(username, permission) 278 .isGranted(Boolean.parseBoolean(grant)) 279 .creator(creator) 280 .begin(begin) 281 .end(end) 282 .build(); 283 acl.add(ace); 284 } 285 acp.addACL(acl); 286 session.setACP(docModel.getRef(), acp, false); 287 } 288 } 289 } 290 } 291 292 @SuppressWarnings("unchecked") 293 protected void loadSchemas(ExportedDocument xdoc, DocumentModel docModel, Document doc) { 294 SchemaManager schemaMgr = Framework.getService(SchemaManager.class); 295 Iterator<Element> it = doc.getRootElement().elementIterator(ExportConstants.SCHEMA_TAG); 296 while (it.hasNext()) { 297 Element element = it.next(); 298 String schemaName = element.attributeValue(ExportConstants.NAME_ATTR); 299 Schema schema = schemaMgr.getSchema(schemaName); 300 if (schema == null) { 301 log.warn("The document " + docModel.getName() + " with id=" + docModel.getId() + " and type=" 302 + docModel.getDocumentType() + " contains the schema '" + schemaName 303 + "', which is not registered as available in the schemaManager. This schema will be ignored."); 304 if (log.isDebugEnabled()) { 305 log.debug("Available schemas: " + Arrays.toString(schemaMgr.getSchemas())); 306 } 307 continue; 308 } 309 loadSchema(xdoc, schema, docModel, element); 310 } 311 } 312 313 @SuppressWarnings("unchecked") 314 protected static void loadSchema(ExportedDocument xdoc, Schema schema, DocumentModel doc, Element schemaElement) { 315 String schemaName = schemaElement.attributeValue(ExportConstants.NAME_ATTR); 316 Map<String, Object> data = new HashMap<>(); 317 Iterator<Element> it = schemaElement.elementIterator(); 318 while (it.hasNext()) { 319 Element element = it.next(); 320 String name = element.getName(); 321 Field field = schema.getField(name); 322 if (field == null) { 323 throw new NuxeoException( 324 "Invalid input document. No such property was found " + name + " in schema " + schemaName); 325 } 326 Object value = getElementData(xdoc, element, field.getType()); 327 data.put(name, value); 328 } 329 doc.setProperties(schemaName, data); 330 } 331 332 protected static Class getFieldClass(Type fieldType) { 333 Class klass = JavaTypes.getClass(fieldType); 334 // for enumerated SimpleTypes we may need to lookup on the supertype 335 // we do the recursion here and not in JavaTypes to avoid potential impacts 336 if (klass == null) { 337 assert fieldType.getSuperType() != null; 338 return getFieldClass(fieldType.getSuperType()); 339 } 340 return klass; 341 } 342 343 @SuppressWarnings("unchecked") 344 private static Object getElementData(ExportedDocument xdoc, Element element, Type type) { 345 // empty xml tag must be null value (not empty string) 346 if (!element.hasContent()) { 347 return null; 348 } 349 if (type.isSimpleType()) { 350 return type.decode(element.getText()); 351 } else if (type.isListType()) { 352 ListType ltype = (ListType) type; 353 List<Object> list = new ArrayList<>(); 354 Iterator<Element> it = element.elementIterator(); 355 while (it.hasNext()) { 356 Element el = it.next(); 357 list.add(getElementData(xdoc, el, ltype.getFieldType())); 358 } 359 Type ftype = ltype.getFieldType(); 360 if (ftype.isSimpleType()) { // these are stored as arrays 361 Class klass = getFieldClass(ftype); 362 if (klass.isPrimitive()) { 363 return PrimitiveArrays.toPrimitiveArray(list, klass); 364 } else { 365 return list.toArray((Object[]) Array.newInstance(klass, list.size())); 366 } 367 } 368 return list; 369 } else { 370 ComplexType ctype = (ComplexType) type; 371 if (TypeConstants.isContentType(ctype)) { 372 String mimeType = element.elementText(ExportConstants.BLOB_MIME_TYPE); 373 String encoding = element.elementText(ExportConstants.BLOB_ENCODING); 374 String content = element.elementTextTrim(ExportConstants.BLOB_DATA); 375 String filename = element.elementTextTrim(ExportConstants.BLOB_FILENAME); 376 if ((content == null || content.length() == 0) && (mimeType == null || mimeType.length() == 0)) { 377 return null; // remove blob 378 } 379 Blob blob = null; 380 if (xdoc.hasExternalBlobs()) { 381 blob = xdoc.getBlob(content); 382 } 383 if (blob == null) { // maybe the blob is embedded in Base64 384 // encoded data 385 byte[] bytes = Base64.decodeBase64(content); 386 blob = Blobs.createBlob(bytes); 387 } 388 blob.setMimeType(mimeType); 389 blob.setEncoding(encoding); 390 blob.setFilename(filename); 391 return blob; 392 } else { // a complex type 393 Map<String, Object> map = new HashMap<>(); 394 Iterator<Element> it = element.elementIterator(); 395 while (it.hasNext()) { 396 Element el = it.next(); 397 String name = el.getName(); 398 Object value = getElementData(xdoc, el, ctype.getField(el.getName()).getType()); 399 map.put(name, value); 400 } 401 return map; 402 } 403 } 404 } 405 406}