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