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        doc = session.createDocument(doc);
155
156        // load into the document the system properties, document needs to exist
157        loadSystemInfo(doc, xdoc.getDocument());
158
159        unsavedDocuments += 1;
160        saveIfNeeded();
161
162        return doc;
163    }
164
165    /**
166     * Updates an existing document.
167     */
168    protected DocumentModel updateDocument(ExportedDocument xdoc, DocumentModel doc) {
169        // load schemas data
170        loadSchemas(xdoc, doc, xdoc.getDocument());
171
172        loadFacetsInfo(doc, xdoc.getDocument());
173
174        doc = session.saveDocument(doc);
175
176        unsavedDocuments += 1;
177        saveIfNeeded();
178
179        return doc;
180    }
181
182    public int getSaveInterval() {
183        return saveInterval;
184    }
185
186    public void setSaveInterval(int saveInterval) {
187        this.saveInterval = saveInterval;
188    }
189
190    @SuppressWarnings("unchecked")
191    protected boolean loadFacetsInfo(DocumentModel docModel, Document doc) {
192        boolean added = false;
193        Element system = doc.getRootElement().element(ExportConstants.SYSTEM_TAG);
194        if (system == null) {
195            return false;
196        }
197
198        Iterator<Element> facets = system.elementIterator(ExportConstants.FACET_TAG);
199        while (facets.hasNext()) {
200            Element element = facets.next();
201            String facet = element.getTextTrim();
202            if (!docModel.hasFacet(facet)) {
203                docModel.addFacet(facet);
204                added = true;
205            }
206        }
207
208        return added;
209    }
210
211    @SuppressWarnings("unchecked")
212    protected void loadSystemInfo(DocumentModel docModel, Document doc) {
213        Element system = doc.getRootElement().element(ExportConstants.SYSTEM_TAG);
214
215        Element accessControl = system.element(ExportConstants.ACCESS_CONTROL_TAG);
216        if (accessControl == null) {
217            return;
218        }
219        Iterator<Element> it = accessControl.elementIterator(ExportConstants.ACL_TAG);
220        while (it.hasNext()) {
221            Element element = it.next();
222            // import only the local acl
223            if (ACL.LOCAL_ACL.equals(element.attributeValue(ExportConstants.NAME_ATTR))) {
224                // this is the local ACL - import it
225                List<Element> entries = element.elements();
226                int size = entries.size();
227                if (size > 0) {
228                    ACP acp = new ACPImpl();
229                    ACL acl = new ACLImpl(ACL.LOCAL_ACL);
230                    acp.addACL(acl);
231                    for (int i = 0; i < size; i++) {
232                        Element el = entries.get(i);
233                        String username = el.attributeValue(ExportConstants.PRINCIPAL_ATTR);
234                        String permission = el.attributeValue(ExportConstants.PERMISSION_ATTR);
235                        String grant = el.attributeValue(ExportConstants.GRANT_ATTR);
236                        String creator = el.attributeValue(ExportConstants.CREATOR_ATTR);
237                        String beginStr = el.attributeValue(ExportConstants.BEGIN_ATTR);
238                        Calendar begin = null;
239                        if (beginStr != null) {
240                            Date date = DateParser.parseW3CDateTime(beginStr);
241                            begin = new GregorianCalendar();
242                            begin.setTimeInMillis(date.getTime());
243                        }
244                        String endStr = el.attributeValue(ExportConstants.END_ATTR);
245                        Calendar end = null;
246                        if (endStr != null) {
247                            Date date = DateParser.parseW3CDateTime(endStr);
248                            end = new GregorianCalendar();
249                            end.setTimeInMillis(date.getTime());
250                        }
251                        ACE ace = ACE.builder(username, permission)
252                                     .isGranted(Boolean.parseBoolean(grant))
253                                     .creator(creator)
254                                     .begin(begin)
255                                     .end(end)
256                                     .build();
257                        acl.add(ace);
258                    }
259                    acp.addACL(acl);
260                    session.setACP(docModel.getRef(), acp, false);
261                }
262            }
263        }
264    }
265
266    @SuppressWarnings("unchecked")
267    protected void loadSchemas(ExportedDocument xdoc, DocumentModel docModel, Document doc) {
268        SchemaManager schemaMgr = Framework.getLocalService(SchemaManager.class);
269        Iterator<Element> it = doc.getRootElement().elementIterator(ExportConstants.SCHEMA_TAG);
270        while (it.hasNext()) {
271            Element element = it.next();
272            String schemaName = element.attributeValue(ExportConstants.NAME_ATTR);
273            Schema schema = schemaMgr.getSchema(schemaName);
274            if (schema == null) {
275                throw new NuxeoException("Schema not found: " + schemaName);
276            }
277            loadSchema(xdoc, schema, docModel, element);
278        }
279    }
280
281    @SuppressWarnings("unchecked")
282    protected static void loadSchema(ExportedDocument xdoc, Schema schema, DocumentModel doc, Element schemaElement) {
283        String schemaName = schemaElement.attributeValue(ExportConstants.NAME_ATTR);
284        Map<String, Object> data = new HashMap<String, Object>();
285        Iterator<Element> it = schemaElement.elementIterator();
286        while (it.hasNext()) {
287            Element element = it.next();
288            String name = element.getName();
289            Field field = schema.getField(name);
290            if (field == null) {
291                throw new NuxeoException("Invalid input document. No such property was found " + name + " in schema "
292                        + schemaName);
293            }
294            Object value = getElementData(xdoc, element, field.getType());
295            data.put(name, value);
296        }
297        doc.setProperties(schemaName, data);
298    }
299
300    protected static Class getFieldClass(Type fieldType) {
301        Class klass = JavaTypes.getClass(fieldType);
302        // for enumerated SimpleTypes we may need to lookup on the supertype
303        // we do the recursion here and not in JavaTypes to avoid potential impacts
304        if (klass == null && fieldType.getSuperType() != null) {
305            return getFieldClass(fieldType.getSuperType());
306        }
307        return klass;
308    }
309
310    @SuppressWarnings("unchecked")
311    private static Object getElementData(ExportedDocument xdoc, Element element, Type type) {
312        // empty xml tag must be null value (not empty string)
313        if (!element.hasContent()) {
314            return null;
315        }
316        if (type.isSimpleType()) {
317            return type.decode(element.getText());
318        } else if (type.isListType()) {
319            ListType ltype = (ListType) type;
320            List<Object> list = new ArrayList<Object>();
321            Iterator<Element> it = element.elementIterator();
322            while (it.hasNext()) {
323                Element el = it.next();
324                list.add(getElementData(xdoc, el, ltype.getFieldType()));
325            }
326            Type ftype = ltype.getFieldType();
327            if (ftype.isSimpleType()) { // these are stored as arrays
328                Class klass = getFieldClass(ftype);
329                if (klass.isPrimitive()) {
330                    return PrimitiveArrays.toPrimitiveArray(list, klass);
331                } else {
332                    return list.toArray((Object[]) Array.newInstance(klass, list.size()));
333                }
334            }
335            return list;
336        } else {
337            ComplexType ctype = (ComplexType) type;
338            if (TypeConstants.isContentType(ctype)) {
339                String mimeType = element.elementText(ExportConstants.BLOB_MIME_TYPE);
340                String encoding = element.elementText(ExportConstants.BLOB_ENCODING);
341                String content = element.elementTextTrim(ExportConstants.BLOB_DATA);
342                String filename = element.elementTextTrim(ExportConstants.BLOB_FILENAME);
343                if ((content == null || content.length() == 0) && (mimeType == null || mimeType.length() == 0)) {
344                    return null; // remove blob
345                }
346                Blob blob = null;
347                if (xdoc.hasExternalBlobs()) {
348                    blob = xdoc.getBlob(content);
349                }
350                if (blob == null) { // maybe the blob is embedded in Base64
351                    // encoded data
352                    byte[] bytes = Base64.decode(content);
353                    blob = Blobs.createBlob(bytes);
354                }
355                blob.setMimeType(mimeType);
356                blob.setEncoding(encoding);
357                blob.setFilename(filename);
358                return blob;
359            } else { // a complex type
360                Map<String, Object> map = new HashMap<String, Object>();
361                Iterator<Element> it = element.elementIterator();
362                while (it.hasNext()) {
363                    Element el = it.next();
364                    String name = el.getName();
365                    Object value = getElementData(xdoc, el, ctype.getField(el.getName()).getType());
366                    map.put(name, value);
367                }
368                return map;
369            }
370        }
371    }
372
373}