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