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}