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