001/*
002 * (C) Copyright 2006-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.lang.reflect.Array;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Calendar;
025import java.util.Date;
026import java.util.GregorianCalendar;
027import java.util.HashMap;
028import java.util.Iterator;
029import java.util.List;
030import java.util.Map;
031
032import org.apache.commons.codec.binary.Base64;
033import org.apache.commons.lang3.StringUtils;
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.dom4j.Document;
037import org.dom4j.Element;
038import org.nuxeo.common.collections.PrimitiveArrays;
039import org.nuxeo.common.utils.Path;
040import org.nuxeo.ecm.core.api.Blob;
041import org.nuxeo.ecm.core.api.Blobs;
042import org.nuxeo.ecm.core.api.CoreSession;
043import org.nuxeo.ecm.core.api.DocumentLocation;
044import org.nuxeo.ecm.core.api.DocumentModel;
045import org.nuxeo.ecm.core.api.NuxeoException;
046import org.nuxeo.ecm.core.api.security.ACE;
047import org.nuxeo.ecm.core.api.security.ACL;
048import org.nuxeo.ecm.core.api.security.ACP;
049import org.nuxeo.ecm.core.api.security.impl.ACLImpl;
050import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
051import org.nuxeo.ecm.core.api.versioning.VersioningService;
052import org.nuxeo.ecm.core.io.ExportConstants;
053import org.nuxeo.ecm.core.io.ExportedDocument;
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.CompositeType;
058import org.nuxeo.ecm.core.schema.types.Field;
059import org.nuxeo.ecm.core.schema.types.JavaTypes;
060import org.nuxeo.ecm.core.schema.types.ListType;
061import org.nuxeo.ecm.core.schema.types.Schema;
062import org.nuxeo.ecm.core.schema.types.Type;
063import org.nuxeo.ecm.core.schema.utils.DateParser;
064import org.nuxeo.runtime.api.Framework;
065
066/**
067 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
068 */
069// TODO: improve it ->
070// modify core session to add a batch create method and use it
071public abstract class AbstractDocumentModelWriter extends AbstractDocumentWriter {
072
073    private static final Log log = LogFactory.getLog(AbstractDocumentModelWriter.class);
074
075    protected CoreSession session;
076
077    protected Path root;
078
079    private int saveInterval;
080
081    protected int unsavedDocuments = 0;
082
083    private final Map<DocumentLocation, DocumentLocation> translationMap = new HashMap<>();
084
085    /**
086     * @param session the session to the repository where to write
087     * @param parentPath where to write the tree. this document will be used as the parent of all top level documents
088     *            passed as input. Note that you may have
089     */
090    protected AbstractDocumentModelWriter(CoreSession session, String parentPath) {
091        this(session, parentPath, 10);
092    }
093
094    protected AbstractDocumentModelWriter(CoreSession session, String parentPath, int saveInterval) {
095        if (session == null) {
096            throw new IllegalArgumentException("null session");
097        }
098        this.session = session;
099        this.saveInterval = saveInterval;
100        root = new Path(parentPath);
101    }
102
103    public Map<DocumentLocation, DocumentLocation> getTranslationMap() {
104        return translationMap;
105    }
106
107    protected void saveIfNeeded() {
108        if (unsavedDocuments >= saveInterval) {
109            session.save();
110            unsavedDocuments = 0;
111        }
112    }
113
114    @Override
115    public void close() {
116        if (unsavedDocuments > 0) {
117            session.save();
118        }
119        session = null;
120        root = null;
121    }
122
123    /**
124     * Creates a new document given its path.
125     * <p>
126     * The parent of this document is assumed to exist.
127     *
128     * @param xdoc the document containing
129     * @param toPath the path of the doc to create
130     */
131    protected DocumentModel createDocument(ExportedDocument xdoc, Path toPath) {
132        Path parentPath = toPath.removeLastSegments(1);
133        String name = toPath.lastSegment();
134
135        DocumentModel doc = session.createDocumentModel(parentPath.toString(), name, xdoc.getType());
136
137        // set lifecycle state at creation
138        Element system = xdoc.getDocument().getRootElement().element(ExportConstants.SYSTEM_TAG);
139        String lifeCycleState = system.element(ExportConstants.LIFECYCLE_STATE_TAG).getText();
140        doc.putContextData("initialLifecycleState", lifeCycleState);
141
142        // loadFacets before schemas so that additional schemas are not skipped
143        loadFacetsInfo(doc, xdoc.getDocument());
144
145        // then load schemas data
146        loadSchemas(xdoc, doc, xdoc.getDocument());
147
148        if (doc.hasSchema("uid")) {
149            doc.putContextData(VersioningService.SKIP_VERSIONING, true);
150        }
151
152        beforeCreateDocument(doc);
153        doc = session.createDocument(doc);
154
155        // load into the document the system properties, document needs to exist
156        loadSystemInfo(doc, xdoc.getDocument());
157
158        unsavedDocuments += 1;
159        saveIfNeeded();
160
161        return doc;
162    }
163
164    /**
165     * @since 8.4
166     */
167    protected void beforeCreateDocument(DocumentModel doc) {
168        // Empty default implementation
169    }
170
171    /**
172     * Updates an existing document.
173     */
174    protected DocumentModel updateDocument(ExportedDocument xdoc, DocumentModel doc) {
175        // load schemas data
176        loadSchemas(xdoc, doc, xdoc.getDocument());
177
178        loadFacetsInfo(doc, xdoc.getDocument());
179
180        beforeSaveDocument(doc);
181        doc = session.saveDocument(doc);
182
183        unsavedDocuments += 1;
184        saveIfNeeded();
185
186        return doc;
187    }
188
189    /**
190     * @since 8.4
191     */
192    protected void beforeSaveDocument(DocumentModel doc) {
193        // Empty default implementation
194    }
195
196    public int getSaveInterval() {
197        return saveInterval;
198    }
199
200    public void setSaveInterval(int saveInterval) {
201        this.saveInterval = saveInterval;
202    }
203
204    @SuppressWarnings("unchecked")
205    protected boolean loadFacetsInfo(DocumentModel docModel, Document doc) {
206        boolean added = false;
207        Element system = doc.getRootElement().element(ExportConstants.SYSTEM_TAG);
208        if (system == null) {
209            return false;
210        }
211
212        Iterator<Element> facets = system.elementIterator(ExportConstants.FACET_TAG);
213        while (facets.hasNext()) {
214            Element element = facets.next();
215            String facet = element.getTextTrim();
216
217            SchemaManager schemaManager = Framework.getService(SchemaManager.class);
218            CompositeType facetType = schemaManager.getFacet(facet);
219
220            if (facetType == null) {
221                log.warn("The document " + docModel.getName() + " with id=" + docModel.getId() + " and type="
222                        + docModel.getDocumentType().getName() + " contains the facet '" + facet
223                        + "', which is not registered as available in the schemaManager. This facet will be ignored.");
224                if (log.isDebugEnabled()) {
225                    log.debug("Available facets: " + Arrays.toString(schemaManager.getFacets()));
226                }
227                continue;
228            }
229
230            if (!docModel.hasFacet(facet)) {
231                docModel.addFacet(facet);
232                added = true;
233            }
234        }
235
236        return added;
237    }
238
239    @SuppressWarnings("unchecked")
240    protected void loadSystemInfo(DocumentModel docModel, Document doc) {
241        Element system = doc.getRootElement().element(ExportConstants.SYSTEM_TAG);
242
243        Element accessControl = system.element(ExportConstants.ACCESS_CONTROL_TAG);
244        if (accessControl == null) {
245            return;
246        }
247        Iterator<Element> it = accessControl.elementIterator(ExportConstants.ACL_TAG);
248        while (it.hasNext()) {
249            Element element = it.next();
250            // import only the local acl
251            if (ACL.LOCAL_ACL.equals(element.attributeValue(ExportConstants.NAME_ATTR))) {
252                // this is the local ACL - import it
253                List<Element> entries = element.elements();
254                int size = entries.size();
255                if (size > 0) {
256                    ACP acp = new ACPImpl();
257                    ACL acl = new ACLImpl(ACL.LOCAL_ACL);
258                    acp.addACL(acl);
259                    for (Element el : entries) {
260                        String username = el.attributeValue(ExportConstants.PRINCIPAL_ATTR);
261                        String permission = el.attributeValue(ExportConstants.PERMISSION_ATTR);
262                        String grant = el.attributeValue(ExportConstants.GRANT_ATTR);
263                        String creator = el.attributeValue(ExportConstants.CREATOR_ATTR);
264                        String beginStr = el.attributeValue(ExportConstants.BEGIN_ATTR);
265                        Calendar begin = null;
266                        if (beginStr != null) {
267                            Date date = DateParser.parseW3CDateTime(beginStr);
268                            begin = new GregorianCalendar();
269                            begin.setTimeInMillis(date.getTime());
270                        }
271                        String endStr = el.attributeValue(ExportConstants.END_ATTR);
272                        Calendar end = null;
273                        if (endStr != null) {
274                            Date date = DateParser.parseW3CDateTime(endStr);
275                            end = new GregorianCalendar();
276                            end.setTimeInMillis(date.getTime());
277                        }
278                        ACE ace = ACE.builder(username, permission)
279                                     .isGranted(Boolean.parseBoolean(grant))
280                                     .creator(creator)
281                                     .begin(begin)
282                                     .end(end)
283                                     .build();
284                        acl.add(ace);
285                    }
286                    acp.addACL(acl);
287                    session.setACP(docModel.getRef(), acp, false);
288                }
289            }
290        }
291    }
292
293    @SuppressWarnings("unchecked")
294    protected void loadSchemas(ExportedDocument xdoc, DocumentModel docModel, Document doc) {
295        SchemaManager schemaMgr = Framework.getService(SchemaManager.class);
296        Iterator<Element> it = doc.getRootElement().elementIterator(ExportConstants.SCHEMA_TAG);
297        while (it.hasNext()) {
298            Element element = it.next();
299            String schemaName = element.attributeValue(ExportConstants.NAME_ATTR);
300            Schema schema = schemaMgr.getSchema(schemaName);
301            if (schema == null) {
302                log.warn("The document " + docModel.getName() + " with id=" + docModel.getId() + " and type="
303                        + docModel.getDocumentType() + " contains the schema '" + schemaName
304                        + "', which is not registered as available in the schemaManager. This schema will be ignored.");
305                if (log.isDebugEnabled()) {
306                    log.debug("Available schemas: " + Arrays.toString(schemaMgr.getSchemas()));
307                }
308                continue;
309            }
310            loadSchema(xdoc, schema, docModel, element);
311        }
312    }
313
314    @SuppressWarnings("unchecked")
315    protected static void loadSchema(ExportedDocument xdoc, Schema schema, DocumentModel doc, Element schemaElement) {
316        String schemaName = schemaElement.attributeValue(ExportConstants.NAME_ATTR);
317        Map<String, Object> data = new HashMap<>();
318        Iterator<Element> it = schemaElement.elementIterator();
319        while (it.hasNext()) {
320            Element element = it.next();
321            String name = element.getName();
322            Field field = schema.getField(name);
323            if (field == null) {
324                throw new NuxeoException(
325                        "Invalid input document. No such property was found " + name + " in schema " + schemaName);
326            }
327            Object value = getElementData(xdoc, element, field.getType());
328            data.put(name, value);
329        }
330        Framework.doPrivileged(() -> doc.setProperties(schemaName, data));
331    }
332
333    protected static Class<?> getFieldClass(Type fieldType) {
334        Class<?> klass = JavaTypes.getClass(fieldType);
335        // for enumerated SimpleTypes we may need to lookup on the supertype
336        // we do the recursion here and not in JavaTypes to avoid potential impacts
337        if (klass == null) {
338            assert fieldType.getSuperType() != null;
339            return getFieldClass(fieldType.getSuperType());
340        }
341        return klass;
342    }
343
344    @SuppressWarnings("unchecked")
345    private static Object getElementData(ExportedDocument xdoc, Element element, Type type) {
346        // empty xml tag must be null value (not empty string)
347        if (!element.hasContent()) {
348            return null;
349        }
350        if (type.isSimpleType()) {
351            return type.decode(element.getText());
352        } else if (type.isListType()) {
353            ListType ltype = (ListType) type;
354            List<Object> list = new ArrayList<>();
355            Iterator<Element> it = element.elementIterator();
356            while (it.hasNext()) {
357                Element el = it.next();
358                list.add(getElementData(xdoc, el, ltype.getFieldType()));
359            }
360            Type ftype = ltype.getFieldType();
361            if (ftype.isSimpleType()) { // these are stored as arrays
362                Class<?> klass = getFieldClass(ftype);
363                if (klass.isPrimitive()) {
364                    return PrimitiveArrays.toPrimitiveArray(list, klass);
365                } else {
366                    return list.toArray((Object[]) Array.newInstance(klass, list.size()));
367                }
368            }
369            return list;
370        } else {
371            ComplexType ctype = (ComplexType) type;
372            if (TypeConstants.isContentType(ctype)) {
373                String mimeType = element.elementText(ExportConstants.BLOB_MIME_TYPE);
374                String encoding = element.elementText(ExportConstants.BLOB_ENCODING);
375                String content = element.elementTextTrim(ExportConstants.BLOB_DATA);
376                String filename = element.elementTextTrim(ExportConstants.BLOB_FILENAME);
377                if ((content == null || content.length() == 0) && (mimeType == null || mimeType.length() == 0)) {
378                    return null; // remove blob
379                }
380                Blob blob = null;
381                if (xdoc.hasExternalBlobs()) {
382                    blob = xdoc.getBlob(content);
383                }
384                if (blob == null) { // maybe the blob is embedded in Base64
385                    // encoded data
386                    byte[] bytes;
387                    try {
388                        bytes = Base64.decodeBase64(content);
389                    } catch (IllegalArgumentException e) {
390                        // example invalid base64: fd7b9e4.blob
391                        if (log.isDebugEnabled()) {
392                            log.warn("Invalid blob base64 in document: " + xdoc.getId() + ": " + StringUtils.abbreviate(content, 50));
393                        } else {
394                            log.warn("Invalid blob base64 in document: " + xdoc.getId());
395                        }
396                        bytes = new byte[0];
397                    }
398                    blob = Blobs.createBlob(bytes);
399                }
400                blob.setMimeType(mimeType);
401                blob.setEncoding(encoding);
402                blob.setFilename(filename);
403                return blob;
404            } else { // a complex type
405                Map<String, Object> map = new HashMap<>();
406                Iterator<Element> it = element.elementIterator();
407                while (it.hasNext()) {
408                    Element el = it.next();
409                    String name = el.getName();
410                    Object value = getElementData(xdoc, el, ctype.getField(el.getName()).getType());
411                    map.put(name, value);
412                }
413                return map;
414            }
415        }
416    }
417
418}