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.getLocalService(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}