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