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}