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.impl.DocumentModelImpl; 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.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.ecm.core.versioning.VersioningService; 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<DocumentLocation, DocumentLocation>(); 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 = new DocumentModelImpl(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.getLocalService(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 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 && 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}