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