001/* 002 * (C) Copyright 2012-2018 Nuxeo (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.io.IOException; 022import java.security.SecureRandom; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Calendar; 026import java.util.Collections; 027import java.util.Comparator; 028import java.util.HashMap; 029import java.util.List; 030import java.util.Map; 031import java.util.Map.Entry; 032import java.util.Random; 033 034import org.apache.commons.codec.binary.Base64; 035import org.apache.commons.lang3.StringUtils; 036import org.dom4j.Document; 037import org.dom4j.DocumentFactory; 038import org.dom4j.Element; 039import org.dom4j.QName; 040import org.nuxeo.common.collections.PrimitiveArrays; 041import org.nuxeo.common.utils.Path; 042import org.nuxeo.ecm.core.api.Blob; 043import org.nuxeo.ecm.core.api.DataModel; 044import org.nuxeo.ecm.core.api.DocumentLocation; 045import org.nuxeo.ecm.core.api.DocumentModel; 046import org.nuxeo.ecm.core.api.IdRef; 047import org.nuxeo.ecm.core.api.impl.DocumentLocationImpl; 048import org.nuxeo.ecm.core.api.security.ACE; 049import org.nuxeo.ecm.core.api.security.ACL; 050import org.nuxeo.ecm.core.api.security.ACP; 051import org.nuxeo.ecm.core.io.ExportConstants; 052import org.nuxeo.ecm.core.io.ExportedDocument; 053import org.nuxeo.ecm.core.schema.Namespace; 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.Field; 058import org.nuxeo.ecm.core.schema.types.ListType; 059import org.nuxeo.ecm.core.schema.types.Schema; 060import org.nuxeo.ecm.core.schema.types.Type; 061import org.nuxeo.ecm.core.schema.utils.DateParser; 062import org.nuxeo.runtime.api.Framework; 063 064/** 065 * A representation for an exported document. 066 * <p> 067 * It contains all the information needed to restore document data and state. 068 * 069 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 070 */ 071public class ExportedDocumentImpl implements ExportedDocument { 072 073 private static final Random RANDOM = new SecureRandom(); 074 075 protected DocumentLocation srcLocation; 076 077 // document unique ID 078 protected String id; 079 080 // document path 081 protected Path path; 082 083 // the main document 084 protected Document document; 085 086 // the external blobs if any 087 protected final Map<String, Blob> blobs = new HashMap<>(4); 088 089 // the optional attached documents 090 protected final Map<String, Document> documents = new HashMap<>(4); 091 092 public ExportedDocumentImpl() { 093 } 094 095 /** 096 * @param path the path to use for this document this is used to remove full paths 097 */ 098 public ExportedDocumentImpl(DocumentModel doc, Path path, boolean inlineBlobs) throws IOException { 099 id = doc.getId(); 100 if (path == null) { 101 this.path = new Path(""); 102 } else { 103 this.path = path.makeRelative(); 104 } 105 readDocument(doc, inlineBlobs); 106 srcLocation = new DocumentLocationImpl(doc); 107 } 108 109 public ExportedDocumentImpl(DocumentModel doc) throws IOException { 110 this(doc, false); 111 } 112 113 public ExportedDocumentImpl(DocumentModel doc, boolean inlineBlobs) throws IOException { 114 this(doc, doc.getPath(), inlineBlobs); 115 } 116 117 /** 118 * @return the source DocumentLocation 119 */ 120 @Override 121 public DocumentLocation getSourceLocation() { 122 return srcLocation; 123 } 124 125 @Override 126 public Path getPath() { 127 return path; 128 } 129 130 @Override 131 public void setPath(Path path) { 132 this.path = path; 133 } 134 135 @Override 136 public String getId() { 137 return id; 138 } 139 140 @Override 141 public void setId(String id) { 142 this.id = id; 143 } 144 145 @Override 146 public String getType() { 147 return document.getRootElement().element(ExportConstants.SYSTEM_TAG).elementText("type"); 148 } 149 150 @Override 151 public Document getDocument() { 152 return document; 153 } 154 155 @Override 156 public void setDocument(Document document) { 157 this.document = document; 158 id = document.getRootElement().attributeValue(ExportConstants.ID_ATTR); 159 String repName = document.getRootElement().attributeValue(ExportConstants.REP_NAME); 160 srcLocation = new DocumentLocationImpl(repName, new IdRef(id)); 161 } 162 163 @Override 164 public Map<String, Blob> getBlobs() { 165 return blobs; 166 } 167 168 @Override 169 public void putBlob(String blobId, Blob blob) { 170 blobs.put(blobId, blob); 171 } 172 173 @Override 174 public Blob removeBlob(String blobId) { 175 return blobs.remove(blobId); 176 } 177 178 @Override 179 public Blob getBlob(String blobId) { 180 return blobs.get(blobId); 181 } 182 183 @Override 184 public boolean hasExternalBlobs() { 185 return !blobs.isEmpty(); 186 } 187 188 @Override 189 public Map<String, Document> getDocuments() { 190 return documents; 191 } 192 193 @Override 194 public Document getDocument(String docId) { 195 return documents.get(docId); 196 } 197 198 @Override 199 public void putDocument(String docId, Document doc) { 200 documents.put(docId, doc); 201 } 202 203 @Override 204 public Document removeDocument(String docId) { 205 return documents.remove(docId); 206 } 207 208 /** 209 * @return the number of files describing the document. 210 */ 211 @Override 212 public int getFilesCount() { 213 return 1 + documents.size() + blobs.size(); 214 } 215 216 protected void readDocument(DocumentModel doc, boolean inlineBlobs) throws IOException { 217 document = DocumentFactory.getInstance().createDocument(); 218 document.setName(doc.getName()); 219 Element rootElement = document.addElement(ExportConstants.DOCUMENT_TAG); 220 rootElement.addAttribute(ExportConstants.REP_NAME, doc.getRepositoryName()); 221 rootElement.addAttribute(ExportConstants.ID_ATTR, doc.getRef().toString()); 222 Element systemElement = rootElement.addElement(ExportConstants.SYSTEM_TAG); 223 systemElement.addElement(ExportConstants.TYPE_TAG).addText(doc.getType()); 224 systemElement.addElement(ExportConstants.PATH_TAG).addText(path.toString()); 225 // lifecycle 226 readLifeCycleInfo(systemElement, doc); 227 228 // facets 229 readFacets(systemElement, doc); 230 // write security 231 Element acpElement = systemElement.addElement(ExportConstants.ACCESS_CONTROL_TAG); 232 ACP acp = doc.getACP(); 233 if (acp != null) { 234 readACP(acpElement, acp); 235 } 236 // write schemas 237 readDocumentSchemas(rootElement, doc, inlineBlobs); 238 } 239 240 protected void readLifeCycleInfo(Element element, DocumentModel doc) { 241 String lifeCycleState = doc.getCurrentLifeCycleState(); 242 if (lifeCycleState != null && lifeCycleState.length() > 0) { 243 element.addElement(ExportConstants.LIFECYCLE_STATE_TAG).addText(lifeCycleState); 244 } 245 String lifeCyclePolicy = doc.getLifeCyclePolicy(); 246 if (lifeCyclePolicy != null && lifeCyclePolicy.length() > 0) { 247 element.addElement(ExportConstants.LIFECYCLE_POLICY_TAG).addText(lifeCyclePolicy); 248 } 249 } 250 251 protected void readFacets(Element element, DocumentModel doc) { 252 // facets 253 List<String> facets = new ArrayList<>(doc.getFacets()); 254 // sort for deterministic order 255 Collections.sort(facets); 256 for (String facet : facets) { 257 element.addElement(ExportConstants.FACET_TAG).addText(facet); 258 } 259 } 260 261 protected void readDocumentSchemas(Element element, DocumentModel doc, boolean inlineBlobs) throws IOException { 262 SchemaManager schemaManager = Framework.getService(SchemaManager.class); 263 List<String> schemaNames = new ArrayList<>(Arrays.asList(doc.getSchemas())); 264 // sort for deterministic order 265 Collections.sort(schemaNames); 266 for (String schemaName : schemaNames) { 267 Element schemaElement = element.addElement(ExportConstants.SCHEMA_TAG).addAttribute("name", schemaName); 268 Schema schema = schemaManager.getSchema(schemaName); 269 Namespace targetNs = schema.getNamespace(); 270 // If namespace prefix is empty, use schema name 271 if (StringUtils.isEmpty(targetNs.prefix)) { 272 targetNs = new Namespace(targetNs.uri, schema.getName()); 273 } 274 schemaElement.addNamespace(targetNs.prefix, targetNs.uri); 275 DataModel dataModel = doc.getDataModel(schemaName); 276 List<Field> fields = new ArrayList<>(schema.getFields()); 277 // sort for deterministic order 278 fields.sort(Comparator.comparing(field -> field.getName().getLocalName())); 279 for (Field field : fields) { 280 Object value = dataModel.getData(field.getName().getLocalName()); 281 readProperty(schemaElement, targetNs, field, value, inlineBlobs); 282 } 283 } 284 285 } 286 287 protected void readProperty(Element parent, Namespace targetNs, Field field, Object value, boolean inlineBlobs) 288 throws IOException { 289 if (value == null) { 290 return; // have no content 291 } 292 Type type = field.getType(); 293 QName name = QName.get(field.getName().getLocalName(), targetNs.prefix, targetNs.uri); 294 Element element = parent.addElement(name); 295 296 // extract the element content 297 if (type.isSimpleType()) { 298 // use CDATA to avoid any bad interaction between content and envelope 299 String encodedValue = type.encode(value); 300 if (requiresCDATA(encodedValue)) { 301 // workaround embedded CDATA 302 encodedValue = encodedValue.replace("]]>", "]]]]><![CDATA[>"); 303 element.addCDATA(encodedValue); 304 } else { 305 element.addText(encodedValue); 306 } 307 } else if (type.isComplexType()) { 308 ComplexType ctype = (ComplexType) type; 309 if (TypeConstants.isContentType(ctype)) { 310 readBlob(element, ctype, (Blob) value, inlineBlobs); 311 } else { 312 readComplex(element, ctype, (Map<String, Object>) value, inlineBlobs); 313 } 314 } else if (type.isListType()) { 315 if (value instanceof List) { 316 readList(element, (ListType) type, (List<Object>) value, inlineBlobs); 317 } else if (value.getClass().getComponentType() != null) { 318 readList(element, (ListType) type, (List<Object>) PrimitiveArrays.toList(value), inlineBlobs); 319 } else { 320 throw new IllegalArgumentException("A value of list type is neither list neither array: " + value); 321 } 322 } 323 } 324 325 protected boolean requiresCDATA(String s) { 326 if (s == null) { 327 return false; 328 } 329 int len = s.length(); 330 for (int i = 0; i < len; i++) { 331 char c = s.charAt(i); 332 if (c < ' ' || c > '~' || c == '<' || c == '&') { 333 return true; 334 } 335 } 336 return false; 337 } 338 339 protected final void readBlob(Element element, ComplexType ctype, Blob blob, boolean inlineBlobs) 340 throws IOException { 341 String blobPath = Integer.toHexString(RANDOM.nextInt()) + ".blob"; 342 element.addElement(ExportConstants.BLOB_ENCODING).addText(blob.getEncoding() != null ? blob.getEncoding() : ""); 343 element.addElement(ExportConstants.BLOB_MIME_TYPE) 344 .addText(blob.getMimeType() != null ? blob.getMimeType() : ""); 345 element.addElement(ExportConstants.BLOB_FILENAME).addText(blob.getFilename() != null ? blob.getFilename() : ""); 346 Element data = element.addElement(ExportConstants.BLOB_DATA); 347 if (inlineBlobs) { 348 String content = Base64.encodeBase64String(blob.getByteArray()); 349 data.setText(content); 350 } else { 351 data.setText(blobPath); 352 blobs.put(blobPath, blob); 353 } 354 element.addElement(ExportConstants.BLOB_DIGEST).addText(blob.getDigest() != null ? blob.getDigest() : ""); 355 } 356 357 protected final void readComplex(Element element, ComplexType ctype, Map<String, Object> map, boolean inlineBlobs) 358 throws IOException { 359 List<Entry<String, Object>> entries = new ArrayList<>(map.entrySet()); 360 // sort for deterministic order 361 entries.sort(Comparator.comparing(Entry::getKey)); 362 for (Entry<String, Object> entry : entries) { 363 readProperty(element, ctype.getNamespace(), ctype.getField(entry.getKey()), entry.getValue(), 364 inlineBlobs); 365 } 366 } 367 368 protected final void readList(Element element, ListType ltype, List<Object> list, boolean inlineBlobs) 369 throws IOException { 370 Field field = ltype.getField(); 371 for (Object obj : list) { 372 readProperty(element, Namespace.DEFAULT_NS, field, obj, inlineBlobs); 373 } 374 } 375 376 protected static void readACP(Element element, ACP acp) { 377 ACL[] acls = acp.getACLs(); 378 for (ACL acl : acls) { 379 Element aclElement = element.addElement(ExportConstants.ACL_TAG); 380 aclElement.addAttribute(ExportConstants.NAME_ATTR, acl.getName()); 381 ACE[] aces = acl.getACEs(); 382 for (ACE ace : aces) { 383 Element aceElement = aclElement.addElement(ExportConstants.ACE_TAG); 384 aceElement.addAttribute(ExportConstants.PRINCIPAL_ATTR, ace.getUsername()); 385 aceElement.addAttribute(ExportConstants.PERMISSION_ATTR, ace.getPermission()); 386 aceElement.addAttribute(ExportConstants.GRANT_ATTR, String.valueOf(ace.isGranted())); 387 aceElement.addAttribute(ExportConstants.CREATOR_ATTR, ace.getCreator()); 388 Calendar begin = ace.getBegin(); 389 if (begin != null) { 390 aceElement.addAttribute(ExportConstants.BEGIN_ATTR, 391 DateParser.formatW3CDateTime((begin).getTime())); 392 } 393 Calendar end = ace.getEnd(); 394 if (end != null) { 395 aceElement.addAttribute(ExportConstants.END_ATTR, DateParser.formatW3CDateTime((end).getTime())); 396 } 397 } 398 } 399 } 400 401}