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