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