001/*
002 * Copyright (c) 2006-2011, 2013 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 *     Bogdan Stefanescu
011 *     Florent Guillaume
012 *     Benoit Delbosc
013 */
014
015package org.nuxeo.ecm.core.api;
016
017import static org.nuxeo.ecm.core.api.event.CoreEventConstants.CHANGED_ACL_NAME;
018import static org.nuxeo.ecm.core.api.event.CoreEventConstants.NEW_ACE;
019import static org.nuxeo.ecm.core.api.event.CoreEventConstants.OLD_ACE;
020import static org.nuxeo.ecm.core.api.security.SecurityConstants.ADD_CHILDREN;
021import static org.nuxeo.ecm.core.api.security.SecurityConstants.BROWSE;
022import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ;
023import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_CHILDREN;
024import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_LIFE_CYCLE;
025import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_PROPERTIES;
026import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_SECURITY;
027import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_VERSION;
028import static org.nuxeo.ecm.core.api.security.SecurityConstants.REMOVE;
029import static org.nuxeo.ecm.core.api.security.SecurityConstants.REMOVE_CHILDREN;
030import static org.nuxeo.ecm.core.api.security.SecurityConstants.SYSTEM_USERNAME;
031import static org.nuxeo.ecm.core.api.security.SecurityConstants.UNLOCK;
032import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE;
033import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_LIFE_CYCLE;
034import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_PROPERTIES;
035import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_SECURITY;
036import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_VERSION;
037
038import java.io.Serializable;
039import java.security.Principal;
040import java.text.DateFormat;
041import java.util.ArrayList;
042import java.util.Arrays;
043import java.util.Collection;
044import java.util.Collections;
045import java.util.Comparator;
046import java.util.Date;
047import java.util.GregorianCalendar;
048import java.util.HashMap;
049import java.util.List;
050import java.util.Map;
051import java.util.Map.Entry;
052
053import org.apache.commons.logging.Log;
054import org.apache.commons.logging.LogFactory;
055import org.nuxeo.common.collections.ScopeType;
056import org.nuxeo.common.collections.ScopedMap;
057import org.nuxeo.ecm.core.CoreService;
058import org.nuxeo.ecm.core.NXCore;
059import org.nuxeo.ecm.core.api.DocumentModel.DocumentModelRefresh;
060import org.nuxeo.ecm.core.api.event.CoreEventConstants;
061import org.nuxeo.ecm.core.api.event.DocumentEventCategories;
062import org.nuxeo.ecm.core.api.event.DocumentEventTypes;
063import org.nuxeo.ecm.core.api.facet.VersioningDocument;
064import org.nuxeo.ecm.core.api.impl.DocumentModelChildrenIterator;
065import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
066import org.nuxeo.ecm.core.api.impl.FacetFilter;
067import org.nuxeo.ecm.core.api.impl.UserPrincipal;
068import org.nuxeo.ecm.core.api.impl.VersionModelImpl;
069import org.nuxeo.ecm.core.api.security.ACE;
070import org.nuxeo.ecm.core.api.security.ACP;
071import org.nuxeo.ecm.core.api.security.SecurityConstants;
072import org.nuxeo.ecm.core.api.security.UserEntry;
073import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
074import org.nuxeo.ecm.core.api.security.impl.UserEntryImpl;
075import org.nuxeo.ecm.core.api.validation.DocumentValidationException;
076import org.nuxeo.ecm.core.api.validation.DocumentValidationReport;
077import org.nuxeo.ecm.core.api.validation.DocumentValidationService;
078import org.nuxeo.ecm.core.event.Event;
079import org.nuxeo.ecm.core.event.EventService;
080import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
081import org.nuxeo.ecm.core.lifecycle.LifeCycleService;
082import org.nuxeo.ecm.core.model.Document;
083import org.nuxeo.ecm.core.model.PathComparator;
084import org.nuxeo.ecm.core.model.Session;
085import org.nuxeo.ecm.core.query.QueryFilter;
086import org.nuxeo.ecm.core.query.QueryParseException;
087import org.nuxeo.ecm.core.query.sql.NXQL;
088import org.nuxeo.ecm.core.query.sql.model.SQLQuery.Transformer;
089import org.nuxeo.ecm.core.schema.DocumentType;
090import org.nuxeo.ecm.core.schema.FacetNames;
091import org.nuxeo.ecm.core.schema.SchemaManager;
092import org.nuxeo.ecm.core.schema.types.CompositeType;
093import org.nuxeo.ecm.core.schema.types.Schema;
094import org.nuxeo.ecm.core.security.SecurityService;
095import org.nuxeo.ecm.core.versioning.VersioningService;
096import org.nuxeo.runtime.api.Framework;
097import org.nuxeo.runtime.metrics.MetricsService;
098
099import com.codahale.metrics.Counter;
100import com.codahale.metrics.MetricRegistry;
101import com.codahale.metrics.SharedMetricRegistries;
102
103/**
104 * Abstract implementation of the client interface.
105 * <p>
106 * This handles all the aspects that are independent on the final implementation (like running inside a J2EE platform or
107 * not).
108 * <p>
109 * The only aspect not implemented is the session management that should be handled by subclasses.
110 *
111 * @author Bogdan Stefanescu
112 * @author Florent Guillaume
113 */
114public abstract class AbstractSession implements CoreSession, Serializable {
115
116    public static final NuxeoPrincipal ANONYMOUS = new UserPrincipal("anonymous", new ArrayList<String>(), true, false);
117
118    private static final Log log = LogFactory.getLog(CoreSession.class);
119
120    private static final long serialVersionUID = 1L;
121
122    private static final Comparator<? super Document> pathComparator = new PathComparator();
123
124    public static final String DEFAULT_MAX_RESULTS = "1000";
125
126    public static final String MAX_RESULTS_PROPERTY = "org.nuxeo.ecm.core.max.results";
127
128    public static final String LIMIT_RESULTS_PROPERTY = "org.nuxeo.ecm.core.limit.results";
129
130    public static final String BINARY_TEXT_SYS_PROP = "fulltextBinary";
131
132    private Boolean limitedResults;
133
134    private Long maxResults;
135
136    // @since 5.7.2
137    protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
138
139    protected Counter createDocumentCount;
140
141    protected Counter deleteDocumentCount;
142
143    protected Counter updateDocumentCount;
144
145    protected void createMetrics() {
146        createDocumentCount = registry.counter(
147                MetricRegistry.name("nuxeo.repositories", getRepositoryName(), "documents", "create"));
148        deleteDocumentCount = registry.counter(
149                MetricRegistry.name("nuxeo.repositories", getRepositoryName(), "documents", "delete"));
150        updateDocumentCount = registry.counter(
151                MetricRegistry.name("nuxeo.repositories", getRepositoryName(), "documents", "update"));
152    }
153
154    /**
155     * Used to check permissions.
156     */
157    private transient SecurityService securityService;
158
159    protected SecurityService getSecurityService() {
160        if (securityService == null) {
161            securityService = NXCore.getSecurityService();
162        }
163        return securityService;
164    }
165
166    private transient VersioningService versioningService;
167
168    protected VersioningService getVersioningService() {
169        if (versioningService == null) {
170            versioningService = Framework.getService(VersioningService.class);
171        }
172        return versioningService;
173    }
174
175    private transient DocumentValidationService validationService;
176
177    protected DocumentValidationService getValidationService() {
178        if (validationService == null) {
179            validationService = Framework.getService(DocumentValidationService.class);
180        }
181        return validationService;
182    }
183
184    /**
185     * Internal method: Gets the current session based on the client session id.
186     *
187     * @return the repository session
188     */
189    public abstract Session getSession();
190
191    @Override
192    public DocumentType getDocumentType(String type) {
193        return Framework.getLocalService(SchemaManager.class).getDocumentType(type);
194    }
195
196    protected final void checkPermission(Document doc, String permission) throws DocumentSecurityException {
197        if (isAdministrator()) {
198            return;
199        }
200        if (!hasPermission(doc, permission)) {
201            log.error("Permission '" + permission + "' is not granted to '" + getPrincipal().getName()
202                    + "' on document " + doc.getPath() + " (" + doc.getUUID() + " - " + doc.getType().getName() + ")");
203            throw new DocumentSecurityException(
204                    "Privilege '" + permission + "' is not granted to '" + getPrincipal().getName() + "'");
205        }
206    }
207
208    protected Map<String, Serializable> getContextMapEventInfo(DocumentModel doc) {
209        Map<String, Serializable> options = new HashMap<String, Serializable>();
210        if (doc != null) {
211            ScopedMap ctxData = doc.getContextData();
212            if (ctxData != null) {
213                options.putAll(ctxData.getDefaultScopeValues());
214                options.putAll(ctxData.getScopeValues(ScopeType.REQUEST));
215            }
216        }
217        return options;
218    }
219
220    public DocumentEventContext newEventContext(DocumentModel source) {
221        DocumentEventContext ctx = new DocumentEventContext(this, getPrincipal(), source);
222        ctx.setProperty(CoreEventConstants.REPOSITORY_NAME, getRepositoryName());
223        ctx.setProperty(CoreEventConstants.SESSION_ID, getSessionId());
224        return ctx;
225    }
226
227    protected void notifyEvent(String eventId, DocumentModel source, Map<String, Serializable> options, String category,
228            String comment, boolean withLifeCycle, boolean inline) {
229
230        DocumentEventContext ctx = new DocumentEventContext(this, getPrincipal(), source);
231
232        // compatibility with old code (< 5.2.M4) - import info from old event
233        // model
234        if (options != null) {
235            ctx.setProperties(options);
236        }
237        ctx.setProperty(CoreEventConstants.REPOSITORY_NAME, getRepositoryName());
238        ctx.setProperty(CoreEventConstants.SESSION_ID, getSessionId());
239        // Document life cycle
240        if (source != null && withLifeCycle) {
241            String currentLifeCycleState = source.getCurrentLifeCycleState();
242            ctx.setProperty(CoreEventConstants.DOC_LIFE_CYCLE, currentLifeCycleState);
243        }
244        if (comment != null) {
245            ctx.setProperty("comment", comment);
246        }
247        ctx.setProperty("category", category == null ? DocumentEventCategories.EVENT_DOCUMENT_CATEGORY : category);
248        // compat code: mark SAVE event as a commit event
249        Event event = ctx.newEvent(eventId);
250        if (DocumentEventTypes.SESSION_SAVED.equals(eventId)) {
251            event.setIsCommitEvent(true);
252        }
253        if (inline) {
254            event.setInline(true);
255        }
256        // compat code: set isLocal on event if JMS is blocked
257        if (source != null) {
258            Boolean blockJms = (Boolean) source.getContextData("BLOCK_JMS_PRODUCING");
259            if (blockJms != null && blockJms) {
260                event.setLocal(true);
261                event.setInline(true);
262            }
263        }
264        Framework.getLocalService(EventService.class).fireEvent(event);
265    }
266
267    /**
268     * Copied from obsolete VersionChangeNotifier.
269     * <p>
270     * Sends change notifications to core event listeners. The event contains info with older document (before version
271     * change) and newer doc (current document).
272     *
273     * @param oldDocument
274     * @param newDocument
275     * @param options additional info to pass to the event
276     */
277    protected void notifyVersionChange(DocumentModel oldDocument, DocumentModel newDocument,
278            Map<String, Serializable> options) {
279        final Map<String, Serializable> info = new HashMap<String, Serializable>();
280        if (options != null) {
281            info.putAll(options);
282        }
283        info.put(VersioningChangeNotifier.EVT_INFO_NEW_DOC_KEY, newDocument);
284        info.put(VersioningChangeNotifier.EVT_INFO_OLD_DOC_KEY, oldDocument);
285        notifyEvent(VersioningChangeNotifier.CORE_EVENT_ID_VERSIONING_CHANGE, newDocument, info,
286                DocumentEventCategories.EVENT_CLIENT_NOTIF_CATEGORY, null, false, false);
287    }
288
289    @Override
290    public boolean hasPermission(Principal principal, DocumentRef docRef, String permission) {
291        Document doc = resolveReference(docRef);
292        return hasPermission(principal, doc, permission);
293    }
294
295    protected final boolean hasPermission(Principal principal, Document doc, String permission) {
296        return getSecurityService().checkPermission(doc, principal, permission);
297    }
298
299    @Override
300    public boolean hasPermission(DocumentRef docRef, String permission) {
301        Document doc = resolveReference(docRef);
302        return hasPermission(doc, permission);
303    }
304
305    protected final boolean hasPermission(Document doc, String permission) {
306        // TODO: optimize this - usually ACP is already available when calling
307        // this method.
308        // -> cache ACP at securitymanager level or try to reuse the ACP when
309        // it is known
310        return getSecurityService().checkPermission(doc, getPrincipal(), permission);
311        // return doc.getSession().getSecurityManager().checkPermission(doc,
312        // getPrincipal().getName(), permission);
313    }
314
315    protected Document resolveReference(DocumentRef docRef) {
316        if (docRef == null) {
317            throw new IllegalArgumentException("null docRref");
318        }
319        Object ref = docRef.reference();
320        if (ref == null) {
321            throw new IllegalArgumentException("null reference");
322        }
323        int type = docRef.type();
324        switch (type) {
325        case DocumentRef.ID:
326            return getSession().getDocumentByUUID((String) ref);
327        case DocumentRef.PATH:
328            return getSession().resolvePath((String) ref);
329        case DocumentRef.INSTANCE:
330            return getSession().getDocumentByUUID(((DocumentModel)ref).getId());
331        default:
332            throw new IllegalArgumentException("Invalid type: " + type);
333        }
334    }
335
336    /**
337     * Gets the document model for the given core document.
338     *
339     * @param doc the document
340     * @return the document model
341     */
342    protected DocumentModel readModel(Document doc) {
343        return DocumentModelFactory.createDocumentModel(doc, getSessionId(), null);
344    }
345
346    /**
347     * Gets the document model for the given core document, preserving the contextData.
348     *
349     * @param doc the document
350     * @return the document model
351     */
352    protected DocumentModel readModel(Document doc, DocumentModel docModel) {
353        DocumentModel newModel = readModel(doc);
354        newModel.copyContextData(docModel);
355        return newModel;
356    }
357
358    protected DocumentModel writeModel(Document doc, DocumentModel docModel) {
359        return DocumentModelFactory.writeDocumentModel(docModel, doc);
360    }
361
362    @Override
363    public DocumentModel copy(DocumentRef src, DocumentRef dst, String name, boolean resetLifeCycle) {
364        Document dstDoc = resolveReference(dst);
365        checkPermission(dstDoc, ADD_CHILDREN);
366
367        Document srcDoc = resolveReference(src);
368        if (name == null) {
369            name = srcDoc.getName();
370        } else {
371            PathRef.checkName(name);
372        }
373
374        Map<String, Serializable> options = new HashMap<String, Serializable>();
375
376        // add the destination name, destination, resetLifeCycle flag and
377        // source references in
378        // the options of the event
379        options.put(CoreEventConstants.SOURCE_REF, src);
380        options.put(CoreEventConstants.DESTINATION_REF, dst);
381        options.put(CoreEventConstants.DESTINATION_PATH, dstDoc.getPath());
382        options.put(CoreEventConstants.DESTINATION_NAME, name);
383        options.put(CoreEventConstants.DESTINATION_EXISTS, dstDoc.hasChild(name));
384        options.put(CoreEventConstants.RESET_LIFECYCLE, resetLifeCycle);
385        DocumentModel srcDocModel = readModel(srcDoc);
386        notifyEvent(DocumentEventTypes.ABOUT_TO_COPY, srcDocModel, options, null, null, true, true);
387
388        name = (String) options.get(CoreEventConstants.DESTINATION_NAME);
389        Document doc = getSession().copy(srcDoc, dstDoc, name);
390        // no need to clear lock, locks table is not copied
391
392        // notify document created by copy
393        DocumentModel docModel = readModel(doc);
394
395        String comment = srcDoc.getRepositoryName() + ':' + src.toString();
396        notifyEvent(DocumentEventTypes.DOCUMENT_CREATED_BY_COPY, docModel, options, null, comment, true, false);
397        docModel = writeModel(doc, docModel);
398
399        // notify document copied
400        comment = doc.getRepositoryName() + ':' + docModel.getRef().toString();
401
402        notifyEvent(DocumentEventTypes.DOCUMENT_DUPLICATED, srcDocModel, options, null, comment, true, false);
403
404        return docModel;
405    }
406
407    @Override
408    public DocumentModel copy(DocumentRef src, DocumentRef dst, String name) {
409        return copy(src, dst, name, false);
410    }
411
412    @Override
413    public List<DocumentModel> copy(List<DocumentRef> src, DocumentRef dst, boolean resetLifeCycle) {
414        List<DocumentModel> newDocuments = new ArrayList<DocumentModel>();
415
416        for (DocumentRef ref : src) {
417            newDocuments.add(copy(ref, dst, null, resetLifeCycle));
418        }
419
420        return newDocuments;
421    }
422
423    @Override
424    public List<DocumentModel> copy(List<DocumentRef> src, DocumentRef dst) {
425        return copy(src, dst, false);
426    }
427
428    @Override
429    public DocumentModel copyProxyAsDocument(DocumentRef src, DocumentRef dst, String name, boolean resetLifeCycle) {
430        Document srcDoc = resolveReference(src);
431        if (!srcDoc.isProxy()) {
432            return copy(src, dst, name);
433        }
434        Document dstDoc = resolveReference(dst);
435        checkPermission(dstDoc, WRITE);
436
437        // create a new document using the expanded proxy
438        DocumentModel srcDocModel = readModel(srcDoc);
439        String docName = (name != null) ? name : srcDocModel.getName();
440        DocumentModel docModel = createDocumentModel(dstDoc.getPath(), docName, srcDocModel.getType());
441        docModel.copyContent(srcDocModel);
442        notifyEvent(DocumentEventTypes.ABOUT_TO_COPY, srcDocModel, null, null, null, true, true);
443        docModel = createDocument(docModel);
444        Document doc = resolveReference(docModel.getRef());
445
446        Map<String, Serializable> options = new HashMap<String, Serializable>();
447        // add resetLifeCycle flag to the event
448        options.put(CoreEventConstants.RESET_LIFECYCLE, resetLifeCycle);
449        // notify document created by copy
450        String comment = srcDoc.getRepositoryName() + ':' + src.toString();
451        notifyEvent(DocumentEventTypes.DOCUMENT_CREATED_BY_COPY, docModel, options, null, comment, true, false);
452
453        // notify document copied
454        comment = doc.getRepositoryName() + ':' + docModel.getRef().toString();
455        notifyEvent(DocumentEventTypes.DOCUMENT_DUPLICATED, srcDocModel, options, null, comment, true, false);
456
457        return docModel;
458    }
459
460    @Override
461    public DocumentModel copyProxyAsDocument(DocumentRef src, DocumentRef dst, String name) {
462        return copyProxyAsDocument(src, dst, name, false);
463    }
464
465    @Override
466    public List<DocumentModel> copyProxyAsDocument(List<DocumentRef> src, DocumentRef dst, boolean resetLifeCycle) {
467        List<DocumentModel> newDocuments = new ArrayList<DocumentModel>();
468
469        for (DocumentRef ref : src) {
470            newDocuments.add(copyProxyAsDocument(ref, dst, null, resetLifeCycle));
471        }
472
473        return newDocuments;
474    }
475
476    @Override
477    public List<DocumentModel> copyProxyAsDocument(List<DocumentRef> src, DocumentRef dst) {
478        return copyProxyAsDocument(src, dst, false);
479    }
480
481    @Override
482    public DocumentModel move(DocumentRef src, DocumentRef dst, String name) {
483        Document srcDoc = resolveReference(src);
484        Document dstDoc;
485        if (dst == null) {
486            // rename
487            dstDoc = srcDoc.getParent();
488            checkPermission(dstDoc, WRITE_PROPERTIES);
489        } else {
490            dstDoc = resolveReference(dst);
491            checkPermission(dstDoc, ADD_CHILDREN);
492            checkPermission(srcDoc.getParent(), REMOVE_CHILDREN);
493            checkPermission(srcDoc, REMOVE);
494        }
495
496        DocumentModel srcDocModel = readModel(srcDoc);
497        String originalName = srcDocModel.getName();
498        if (name == null) {
499            name = srcDocModel.getName();
500        } else {
501            PathRef.checkName(name);
502        }
503        Map<String, Serializable> options = getContextMapEventInfo(srcDocModel);
504        // add the destination name, destination and source references in
505        // the options of the event
506        options.put(CoreEventConstants.SOURCE_REF, src);
507        options.put(CoreEventConstants.DESTINATION_REF, dst);
508        options.put(CoreEventConstants.DESTINATION_PATH, dstDoc.getPath());
509        options.put(CoreEventConstants.DESTINATION_NAME, name);
510        options.put(CoreEventConstants.DESTINATION_EXISTS, dstDoc.hasChild(name));
511
512        notifyEvent(DocumentEventTypes.ABOUT_TO_MOVE, srcDocModel, options, null, null, true, true);
513
514        name = (String) options.get(CoreEventConstants.DESTINATION_NAME);
515
516        if (!originalName.equals(name)) {
517            options.put(CoreEventConstants.ORIGINAL_NAME, originalName);
518        }
519
520        String comment = srcDoc.getRepositoryName() + ':' + srcDoc.getParent().getUUID();
521
522        Document doc = getSession().move(srcDoc, dstDoc, name);
523
524        // notify document moved
525        DocumentModel docModel = readModel(doc);
526        options.put(CoreEventConstants.PARENT_PATH, srcDocModel.getParentRef());
527        notifyEvent(DocumentEventTypes.DOCUMENT_MOVED, docModel, options, null, comment, true, false);
528
529        return docModel;
530    }
531
532    @Override
533    public void move(List<DocumentRef> src, DocumentRef dst) {
534        for (DocumentRef ref : src) {
535            move(ref, dst, null);
536        }
537    }
538
539    @Override
540    public ACP getACP(DocumentRef docRef) {
541        Document doc = resolveReference(docRef);
542        checkPermission(doc, READ_SECURITY);
543        return getSession().getMergedACP(doc);
544    }
545
546    @Override
547    public void setACP(DocumentRef docRef, ACP newAcp, boolean overwrite) {
548        Document doc = resolveReference(docRef);
549        checkPermission(doc, WRITE_SECURITY);
550
551        setACP(doc, newAcp, overwrite, null);
552    }
553
554    protected void setACP(Document doc, ACP newAcp, boolean overwrite, Map<String, Serializable> options) {
555        DocumentModel docModel = readModel(doc);
556        if (options == null) {
557            options = new HashMap<>();
558        }
559        options.put(CoreEventConstants.OLD_ACP, docModel.getACP().clone());
560        options.put(CoreEventConstants.NEW_ACP, newAcp);
561
562        notifyEvent(DocumentEventTypes.BEFORE_DOC_SECU_UPDATE, docModel, options, null, null, true, true);
563        getSession().setACP(doc, newAcp, overwrite);
564        docModel = readModel(doc);
565        options.put(CoreEventConstants.NEW_ACP, newAcp.clone());
566        notifyEvent(DocumentEventTypes.DOCUMENT_SECURITY_UPDATED, docModel, options, null, null, true, false);
567    }
568
569    @Override
570    public void replaceACE(DocumentRef docRef, String aclName, ACE oldACE, ACE newACE) {
571        Document doc = resolveReference(docRef);
572        checkPermission(doc, WRITE_SECURITY);
573
574        ACP acp = getACP(docRef);
575        if (acp.replaceACE(aclName, oldACE, newACE)) {
576            Map<String, Serializable> options = new HashMap<>();
577            options.put(OLD_ACE, oldACE);
578            options.put(NEW_ACE, newACE);
579            options.put(CHANGED_ACL_NAME, aclName);
580            setACP(doc, acp, true, options);
581        }
582    }
583
584    @Override
585    public boolean isNegativeAclAllowed() {
586        return getSession().isNegativeAclAllowed();
587    }
588
589    @Override
590    public void cancel() {
591        // nothing
592    }
593
594    private DocumentModel createDocumentModelFromTypeName(String typeName, Map<String, Serializable> options) {
595        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
596        DocumentType docType = schemaManager.getDocumentType(typeName);
597        if (docType == null) {
598            throw new IllegalArgumentException(typeName + " is not a registered core type");
599        }
600        DocumentModel docModel = DocumentModelFactory.createDocumentModel(getSessionId(), docType);
601        if (options == null) {
602            options = new HashMap<String, Serializable>();
603        }
604        // do not forward this event on the JMS Bus
605        options.put("BLOCK_JMS_PRODUCING", true);
606        notifyEvent(DocumentEventTypes.EMPTY_DOCUMENTMODEL_CREATED, docModel, options, null, null, false, true);
607        return docModel;
608    }
609
610    @Override
611    public DocumentModel createDocumentModel(String typeName) {
612        Map<String, Serializable> options = new HashMap<String, Serializable>();
613        return createDocumentModelFromTypeName(typeName, options);
614    }
615
616    @Override
617    public DocumentModel createDocumentModel(String parentPath, String name, String typeName) {
618        Map<String, Serializable> options = new HashMap<String, Serializable>();
619        options.put(CoreEventConstants.PARENT_PATH, parentPath);
620        options.put(CoreEventConstants.DOCUMENT_MODEL_ID, name);
621        options.put(CoreEventConstants.DESTINATION_NAME, name);
622        DocumentModel model = createDocumentModelFromTypeName(typeName, options);
623        model.setPathInfo(parentPath, name);
624        return model;
625    }
626
627    @Override
628    public DocumentModel createDocumentModel(String typeName, Map<String, Object> options) {
629
630        Map<String, Serializable> serializableOptions = new HashMap<String, Serializable>();
631
632        for (Entry<String, Object> entry : options.entrySet()) {
633            serializableOptions.put(entry.getKey(), (Serializable) entry.getValue());
634        }
635        return createDocumentModelFromTypeName(typeName, serializableOptions);
636    }
637
638    @Override
639    public DocumentModel createDocument(DocumentModel docModel) {
640        if (docModel.getSessionId() == null) {
641            // docModel was created using constructor instead of CoreSession.createDocumentModel
642            docModel.attach(getSessionId());
643        }
644        String typeName = docModel.getType();
645        DocumentRef parentRef = docModel.getParentRef();
646        if (typeName == null) {
647            throw new NullPointerException("null typeName");
648        }
649        if (parentRef == null && !isAdministrator()) {
650            throw new NuxeoException("Only Administrators can create placeless documents");
651        }
652        String childName = docModel.getName();
653        Map<String, Serializable> options = getContextMapEventInfo(docModel);
654
655        // document validation
656        if (getValidationService().isActivated(DocumentValidationService.CTX_CREATEDOC, options)) {
657            DocumentValidationReport report = getValidationService().validate(docModel, true);
658            if (report.hasError()) {
659                throw new DocumentValidationException(report);
660            }
661        }
662
663        Document folder = fillCreateOptions(parentRef, childName, options);
664
665        // get initial life cycle state info
666        String initialLifecycleState = null;
667        Object lifecycleStateInfo = docModel.getContextData(LifeCycleConstants.INITIAL_LIFECYCLE_STATE_OPTION_NAME);
668        if (lifecycleStateInfo instanceof String) {
669            initialLifecycleState = (String) lifecycleStateInfo;
670        }
671        notifyEvent(DocumentEventTypes.ABOUT_TO_CREATE, docModel, options, null, null, false, true); // no lifecycle
672                                                                                                     // yet
673        childName = (String) options.get(CoreEventConstants.DESTINATION_NAME);
674        Document doc = folder.addChild(childName, typeName);
675
676        // update facets too since some of them may be dynamic
677        for (String facetName : docModel.getFacets()) {
678            if (!doc.getAllFacets().contains(facetName) && !FacetNames.IMMUTABLE.equals(facetName)) {
679                doc.addFacet(facetName);
680            }
681        }
682
683        // init document life cycle
684        NXCore.getLifeCycleService().initialize(doc, initialLifecycleState);
685
686        // init document with data from doc model
687        docModel = writeModel(doc, docModel);
688
689        if (!Boolean.TRUE.equals(docModel.getContextData(ScopeType.REQUEST, VersioningService.SKIP_VERSIONING))) {
690            // during remote publishing we want to skip versioning
691            // to avoid overwriting the version number
692            getVersioningService().doPostCreate(doc, options);
693            docModel = readModel(doc, docModel);
694        }
695
696        notifyEvent(DocumentEventTypes.DOCUMENT_CREATED, docModel, options, null, null, true, false);
697        docModel = writeModel(doc, docModel);
698
699        createDocumentCount.inc();
700        return docModel;
701    }
702
703    protected Document fillCreateOptions(DocumentRef parentRef, String childName, Map<String, Serializable> options)
704            throws DocumentSecurityException {
705        Document folder;
706        if (parentRef == null || EMPTY_PATH.equals(parentRef)) {
707            folder = getSession().getNullDocument();
708            options.put(CoreEventConstants.DESTINATION_REF, null);
709            options.put(CoreEventConstants.DESTINATION_PATH, null);
710            options.put(CoreEventConstants.DESTINATION_NAME, childName);
711            options.put(CoreEventConstants.DESTINATION_EXISTS, false);
712        } else {
713            folder = resolveReference(parentRef);
714            checkPermission(folder, ADD_CHILDREN);
715            options.put(CoreEventConstants.DESTINATION_REF, parentRef);
716            options.put(CoreEventConstants.DESTINATION_PATH, folder.getPath());
717            options.put(CoreEventConstants.DESTINATION_NAME, childName);
718            options.put(CoreEventConstants.DESTINATION_EXISTS, folder.hasChild(childName));
719        }
720        return folder;
721    }
722
723    @Override
724    public void importDocuments(List<DocumentModel> docModels) {
725        for (DocumentModel docModel : docModels) {
726            importDocument(docModel);
727        }
728    }
729
730    protected static final PathRef EMPTY_PATH = new PathRef("");
731
732    protected void importDocument(DocumentModel docModel) {
733        if (!isAdministrator()) {
734            throw new DocumentSecurityException("Only Administrator can import");
735        }
736        String name = docModel.getName();
737        if (name == null || name.length() == 0) {
738            throw new IllegalArgumentException("Invalid empty name");
739        }
740        String typeName = docModel.getType();
741        if (typeName == null || typeName.length() == 0) {
742            throw new IllegalArgumentException("Invalid empty type");
743        }
744        String id = docModel.getId();
745        if (id == null || id.length() == 0) {
746            throw new IllegalArgumentException("Invalid empty id");
747        }
748
749        DocumentRef parentRef = docModel.getParentRef();
750        Map<String, Serializable> props = getContextMapEventInfo(docModel);
751
752        // document validation
753        if (getValidationService().isActivated(DocumentValidationService.CTX_IMPORTDOC, props)) {
754            DocumentValidationReport report = getValidationService().validate(docModel, true);
755            if (report.hasError()) {
756                throw new DocumentValidationException(report);
757            }
758        }
759
760        if (parentRef != null && EMPTY_PATH.equals(parentRef)) {
761            parentRef = null;
762        }
763        Document parent = fillCreateOptions(parentRef, name, props);
764        notifyEvent(DocumentEventTypes.ABOUT_TO_IMPORT, docModel, props, null, null, false, true);
765        name = (String) props.get(CoreEventConstants.DESTINATION_NAME);
766
767        // create the document
768        Document doc = getSession().importDocument(id, parentRef == null ? null : parent, name, typeName, props);
769
770        if (typeName.equals(CoreSession.IMPORT_PROXY_TYPE)) {
771            // just reread the final document
772            docModel = readModel(doc);
773        } else {
774            // init document with data from doc model
775            docModel = writeModel(doc, docModel);
776        }
777
778        // send an event about the import
779        notifyEvent(DocumentEventTypes.DOCUMENT_IMPORTED, docModel, null, null, null, true, false);
780    }
781
782    @Override
783    public DocumentModel[] createDocument(DocumentModel[] docModels) {
784        DocumentModel[] models = new DocumentModel[docModels.length];
785        int i = 0;
786        // TODO: optimize this (do not call at each iteration createDocument())
787        for (DocumentModel docModel : docModels) {
788            models[i++] = createDocument(docModel);
789        }
790        return models;
791    }
792
793    @Override
794    public boolean exists(DocumentRef docRef) {
795        try {
796            Document doc = resolveReference(docRef);
797            return hasPermission(doc, BROWSE);
798        } catch (DocumentNotFoundException e) {
799            return false;
800        }
801    }
802
803    @Override
804    public DocumentModel getChild(DocumentRef parent, String name) {
805        Document doc = resolveReference(parent);
806        checkPermission(doc, READ_CHILDREN);
807        Document child = doc.getChild(name);
808        checkPermission(child, READ);
809        return readModel(child);
810    }
811
812    @Override
813    public boolean hasChild(DocumentRef parent, String name) {
814        Document doc = resolveReference(parent);
815        checkPermission(doc, READ_CHILDREN);
816        return doc.hasChild(name);
817    }
818
819    @Override
820    public DocumentModelList getChildren(DocumentRef parent) {
821        return getChildren(parent, null, READ, null, null);
822    }
823
824    @Override
825    public DocumentModelList getChildren(DocumentRef parent, String type) {
826        return getChildren(parent, type, READ, null, null);
827    }
828
829    @Override
830    public DocumentModelList getChildren(DocumentRef parent, String type, String perm) {
831        return getChildren(parent, type, perm, null, null);
832    }
833
834    @Override
835    public DocumentModelList getChildren(DocumentRef parent, String type, Filter filter, Sorter sorter) {
836        return getChildren(parent, type, null, filter, sorter);
837    }
838
839    @Override
840    public DocumentModelList getChildren(DocumentRef parent, String type, String perm, Filter filter, Sorter sorter) {
841        if (perm == null) {
842            perm = READ;
843        }
844        Document doc = resolveReference(parent);
845        checkPermission(doc, READ_CHILDREN);
846        DocumentModelList docs = new DocumentModelListImpl();
847        for (Document child : doc.getChildren()) {
848            if (hasPermission(child, perm)) {
849                if (child.getType() != null && (type == null || type.equals(child.getType().getName()))) {
850                    DocumentModel childModel = readModel(child);
851                    if (filter == null || filter.accept(childModel)) {
852                        docs.add(childModel);
853                    }
854                }
855            }
856        }
857        if (sorter != null) {
858            Collections.sort(docs, sorter);
859        }
860        return docs;
861    }
862
863    @Override
864    public List<DocumentRef> getChildrenRefs(DocumentRef parentRef, String perm) {
865        if (perm != null) {
866            // XXX TODO
867            throw new NullPointerException("perm != null not implemented");
868        }
869        Document parent = resolveReference(parentRef);
870        checkPermission(parent, READ_CHILDREN);
871        List<String> ids = parent.getChildrenIds();
872        List<DocumentRef> refs = new ArrayList<DocumentRef>(ids.size());
873        for (String id : ids) {
874            refs.add(new IdRef(id));
875        }
876        return refs;
877    }
878
879    @Override
880    public DocumentModelIterator getChildrenIterator(DocumentRef parent) {
881        return getChildrenIterator(parent, null, null, null);
882    }
883
884    @Override
885    public DocumentModelIterator getChildrenIterator(DocumentRef parent, String type) {
886        return getChildrenIterator(parent, type, null, null);
887    }
888
889    @Override
890    public DocumentModelIterator getChildrenIterator(DocumentRef parent, String type, String perm, Filter filter) {
891        // perm unused, kept for API compat
892        return new DocumentModelChildrenIterator(this, parent, type, filter);
893    }
894
895    @Override
896    public DocumentModel getDocument(DocumentRef docRef) {
897        Document doc = resolveReference(docRef);
898        checkPermission(doc, READ);
899        return readModel(doc);
900    }
901
902    @Override
903    public DocumentModelList getDocuments(DocumentRef[] docRefs) {
904        List<DocumentModel> docs = new ArrayList<DocumentModel>(docRefs.length);
905        for (DocumentRef docRef : docRefs) {
906            Document doc;
907            try {
908                doc = resolveReference(docRef);
909                checkPermission(doc, READ);
910            } catch (DocumentSecurityException e) {
911                // no permission
912                continue;
913            }
914            docs.add(readModel(doc));
915        }
916        return new DocumentModelListImpl(docs);
917    }
918
919    @Override
920    public DocumentModelList getFiles(DocumentRef parent) {
921        Document doc = resolveReference(parent);
922        checkPermission(doc, READ_CHILDREN);
923        DocumentModelList docs = new DocumentModelListImpl();
924        for (Document child : doc.getChildren()) {
925            if (!child.isFolder() && hasPermission(child, READ)) {
926                docs.add(readModel(child));
927            }
928        }
929        return docs;
930    }
931
932    @Override
933    public DocumentModelList getFiles(DocumentRef parent, Filter filter, Sorter sorter) {
934        Document doc = resolveReference(parent);
935        checkPermission(doc, READ_CHILDREN);
936        DocumentModelList docs = new DocumentModelListImpl();
937        for (Document child : doc.getChildren()) {
938            if (!child.isFolder() && hasPermission(child, READ)) {
939                DocumentModel docModel = readModel(doc);
940                if (filter == null || filter.accept(docModel)) {
941                    docs.add(readModel(child));
942                }
943            }
944        }
945        if (sorter != null) {
946            Collections.sort(docs, sorter);
947        }
948        return docs;
949    }
950
951    @Override
952    public DocumentModelList getFolders(DocumentRef parent) {
953        Document doc = resolveReference(parent);
954        checkPermission(doc, READ_CHILDREN);
955        DocumentModelList docs = new DocumentModelListImpl();
956        for (Document child : doc.getChildren()) {
957            if (child.isFolder() && hasPermission(child, READ)) {
958                docs.add(readModel(child));
959            }
960        }
961        return docs;
962    }
963
964    @Override
965    public DocumentModelList getFolders(DocumentRef parent, Filter filter, Sorter sorter) {
966        Document doc = resolveReference(parent);
967        checkPermission(doc, READ_CHILDREN);
968        DocumentModelList docs = new DocumentModelListImpl();
969        for (Document child : doc.getChildren()) {
970            if (child.isFolder() && hasPermission(child, READ)) {
971                DocumentModel childModel = readModel(child);
972                if (filter == null || filter.accept(childModel)) {
973                    docs.add(childModel);
974                }
975            }
976        }
977        if (sorter != null) {
978            Collections.sort(docs, sorter);
979        }
980        return docs;
981    }
982
983    @Override
984    public DocumentRef getParentDocumentRef(DocumentRef docRef) {
985        final Document doc = resolveReference(docRef);
986        Document parentDoc = doc.getParent();
987        return parentDoc != null ? new IdRef(parentDoc.getUUID()) : null;
988    }
989
990    @Override
991    public DocumentModel getParentDocument(DocumentRef docRef) {
992        Document doc = resolveReference(docRef);
993        Document parentDoc = doc.getParent();
994        if (parentDoc == null) {
995            return null;
996        }
997        if (!hasPermission(parentDoc, READ)) {
998            throw new DocumentSecurityException("Privilege READ is not granted to " + getPrincipal().getName());
999        }
1000        return readModel(parentDoc);
1001    }
1002
1003    @Override
1004    public List<DocumentModel> getParentDocuments(final DocumentRef docRef) {
1005
1006        if (null == docRef) {
1007            throw new IllegalArgumentException("null docRef");
1008        }
1009
1010        final List<DocumentModel> docsList = new ArrayList<DocumentModel>();
1011        Document doc = resolveReference(docRef);
1012        while (doc != null && !"/".equals(doc.getPath())) {
1013            // XXX OG: shouldn't we check BROWSE and READ_PROPERTIES
1014            // instead?
1015            if (!hasPermission(doc, READ)) {
1016                break;
1017            }
1018            docsList.add(readModel(doc));
1019            doc = doc.getParent();
1020        }
1021        Collections.reverse(docsList);
1022        return docsList;
1023    }
1024
1025    @Override
1026    public DocumentModel getRootDocument() {
1027        return readModel(getSession().getRootDocument());
1028    }
1029
1030    @Override
1031    public boolean hasChildren(DocumentRef docRef) {
1032        // TODO: validate permission check with td
1033        Document doc = resolveReference(docRef);
1034        checkPermission(doc, BROWSE);
1035        return doc.hasChildren();
1036    }
1037
1038    @Override
1039    public DocumentModelList query(String query) {
1040        return query(query, null, 0, 0, false);
1041    }
1042
1043    @Override
1044    public DocumentModelList query(String query, int max) {
1045        return query(query, null, max, 0, false);
1046    }
1047
1048    @Override
1049    public DocumentModelList query(String query, Filter filter) {
1050        return query(query, filter, 0, 0, false);
1051    }
1052
1053    @Override
1054    public DocumentModelList query(String query, Filter filter, int max) {
1055        return query(query, filter, max, 0, false);
1056    }
1057
1058    @Override
1059    public DocumentModelList query(String query, Filter filter, long limit, long offset, boolean countTotal) {
1060        return query(query, NXQL.NXQL, filter, limit, offset, countTotal);
1061    }
1062
1063    @Override
1064    public DocumentModelList query(String query, String queryType, Filter filter, long limit, long offset,
1065            boolean countTotal) {
1066        long countUpTo;
1067        if (!countTotal) {
1068            countUpTo = 0;
1069        } else {
1070            if (isLimitedResults()) {
1071                countUpTo = getMaxResults();
1072            } else {
1073                countUpTo = -1;
1074            }
1075        }
1076        return query(query, queryType, filter, limit, offset, countUpTo);
1077    }
1078
1079    protected long getMaxResults() {
1080        if (maxResults == null) {
1081            maxResults = Long.parseLong(Framework.getProperty(MAX_RESULTS_PROPERTY, DEFAULT_MAX_RESULTS));
1082        }
1083        return maxResults;
1084    }
1085
1086    protected boolean isLimitedResults() {
1087        if (limitedResults == null) {
1088            limitedResults = Boolean.parseBoolean(Framework.getProperty(LIMIT_RESULTS_PROPERTY));
1089        }
1090        return limitedResults;
1091    }
1092
1093    protected void setMaxResults(long maxResults) {
1094        this.maxResults = maxResults;
1095    }
1096
1097    protected void setLimitedResults(boolean limitedResults) {
1098        this.limitedResults = limitedResults;
1099    }
1100
1101    @Override
1102    public DocumentModelList query(String query, Filter filter, long limit, long offset, long countUpTo) {
1103        return query(query, NXQL.NXQL, filter, limit, offset, countUpTo);
1104    }
1105
1106    @Override
1107    public DocumentModelList query(String query, String queryType, Filter filter, long limit, long offset,
1108            long countUpTo) {
1109        SecurityService securityService = getSecurityService();
1110        Principal principal = getPrincipal();
1111        try {
1112            String permission = BROWSE;
1113            String repoName = getRepositoryName();
1114            boolean postFilterPolicies = !securityService.arePoliciesExpressibleInQuery(repoName);
1115            boolean postFilterFilter = filter != null && !(filter instanceof FacetFilter);
1116            boolean postFilter = postFilterPolicies || postFilterFilter;
1117            String[] principals;
1118            if (isAdministrator()) {
1119                principals = null; // means: no security check needed
1120            } else {
1121                principals = SecurityService.getPrincipalsToCheck(principal);
1122            }
1123            String[] permissions = securityService.getPermissionsToCheck(permission);
1124            QueryFilter queryFilter = new QueryFilter(principal, principals, permissions,
1125                    filter instanceof FacetFilter ? (FacetFilter) filter : null,
1126                    securityService.getPoliciesQueryTransformers(repoName), postFilter ? 0 : limit,
1127                    postFilter ? 0 : offset);
1128
1129            // get document list with total size
1130            PartialList<Document> pl = getSession().query(query, queryType, queryFilter, postFilter ? -1 : countUpTo);
1131            // convert to DocumentModelList
1132            DocumentModelListImpl dms = new DocumentModelListImpl(pl.list.size());
1133            dms.setTotalSize(pl.totalSize);
1134            for (Document doc : pl.list) {
1135                dms.add(readModel(doc));
1136            }
1137
1138            if (!postFilter) {
1139                // the backend has done all the needed filtering
1140                return dms;
1141            }
1142
1143            // post-filter the results "by hand", the backend couldn't do it
1144            long start = limit == 0 || offset < 0 ? 0 : offset;
1145            long stop = start + (limit == 0 ? dms.size() : limit);
1146            int n = 0;
1147            DocumentModelListImpl docs = new DocumentModelListImpl();
1148            for (DocumentModel model : dms) {
1149                if (postFilterPolicies) {
1150                    if (!hasPermission(model.getRef(), permission)) {
1151                        continue;
1152                    }
1153                }
1154                if (postFilterFilter) {
1155                    if (!filter.accept(model)) {
1156                        continue;
1157                    }
1158                }
1159                if (n < start) {
1160                    n++;
1161                    continue;
1162                }
1163                if (n >= stop) {
1164                    if (countUpTo == 0) {
1165                        // can break early
1166                        break;
1167                    }
1168                    n++;
1169                    continue;
1170                }
1171                n++;
1172                docs.add(model);
1173            }
1174            if (countUpTo != 0) {
1175                docs.setTotalSize(n);
1176            }
1177            return docs;
1178        } catch (QueryParseException e) {
1179            e.addInfo("Failed to execute query: " + query);
1180            throw e;
1181        }
1182    }
1183
1184    @Override
1185    public IterableQueryResult queryAndFetch(String query, String queryType, Object... params) {
1186        try {
1187            SecurityService securityService = getSecurityService();
1188            Principal principal = getPrincipal();
1189            String[] principals;
1190            if (isAdministrator()) {
1191                principals = null; // means: no security check needed
1192            } else {
1193                principals = SecurityService.getPrincipalsToCheck(principal);
1194            }
1195            String permission = BROWSE;
1196            String[] permissions = securityService.getPermissionsToCheck(permission);
1197            Collection<Transformer> transformers;
1198            if (NXQL.NXQL.equals(queryType)) {
1199                String repoName = getRepositoryName();
1200                transformers = securityService.getPoliciesQueryTransformers(repoName);
1201            } else {
1202                transformers = Collections.emptyList();
1203            }
1204            QueryFilter queryFilter = new QueryFilter(principal, principals, permissions, null, transformers, 0, 0);
1205            IterableQueryResult result = getSession().queryAndFetch(query, queryType, queryFilter, params);
1206            return result;
1207        } catch (QueryParseException e) {
1208            e.addInfo("Failed to execute query: " + queryType + ": " + query);
1209            throw e;
1210        }
1211    }
1212
1213    @Override
1214    public void removeChildren(DocumentRef docRef) {
1215        // TODO: check req permissions with td
1216        Document doc = resolveReference(docRef);
1217        checkPermission(doc, REMOVE_CHILDREN);
1218        List<Document> children = doc.getChildren();
1219        // remove proxies first, otherwise they could become dangling
1220        for (Document child : children) {
1221            if (child.isProxy()) {
1222                if (hasPermission(child, REMOVE)) {
1223                    removeNotifyOneDoc(child);
1224                }
1225            }
1226        }
1227        // then remove regular docs or versions, both of which could be proxies targets
1228        for (Document child : children) {
1229            if (!child.isProxy()) {
1230                if (hasPermission(child, REMOVE)) {
1231                    removeNotifyOneDoc(child);
1232                }
1233            }
1234        }
1235    }
1236
1237    @Override
1238    public boolean canRemoveDocument(DocumentRef docRef) {
1239        Document doc = resolveReference(docRef);
1240        return canRemoveDocument(doc) == null;
1241    }
1242
1243    /**
1244     * Checks if a document can be removed, and returns a failure reason if not.
1245     */
1246    protected String canRemoveDocument(Document doc) {
1247        // TODO must also check for proxies on live docs
1248        if (doc.isVersion()) {
1249            // TODO a hasProxies method would be more efficient
1250            Collection<Document> proxies = getSession().getProxies(doc, null);
1251            if (!proxies.isEmpty()) {
1252                return "Proxy " + proxies.iterator().next().getUUID() + " targets version " + doc.getUUID();
1253            }
1254            // find a working document to check security
1255            Document working = doc.getSourceDocument();
1256            if (working != null) {
1257                Document baseVersion = working.getBaseVersion();
1258                if (baseVersion != null && !baseVersion.isCheckedOut() && baseVersion.getUUID().equals(doc.getUUID())) {
1259                    return "Working copy " + working.getUUID() + " is checked in with base version " + doc.getUUID();
1260                }
1261                return hasPermission(working, WRITE_VERSION) ? null
1262                        : "Missing permission '" + WRITE_VERSION + "' on working copy " + working.getUUID();
1263            } else {
1264                // no working document, only admins can remove
1265                return isAdministrator() ? null : "No working copy and not an Administrator";
1266            }
1267        } else {
1268            if (isAdministrator()) {
1269                return null; // ok
1270            }
1271            if (!hasPermission(doc, REMOVE)) {
1272                return "Missing permission '" + REMOVE + "' on document " + doc.getUUID();
1273            }
1274            Document parent = doc.getParent();
1275            if (parent == null) {
1276                return null; // ok
1277            }
1278            return hasPermission(parent, REMOVE_CHILDREN) ? null
1279                    : "Missing permission '" + REMOVE_CHILDREN + "' on parent document " + parent.getUUID();
1280        }
1281    }
1282
1283    @Override
1284    public void removeDocument(DocumentRef docRef) {
1285        Document doc = resolveReference(docRef);
1286        removeDocument(doc);
1287    }
1288
1289    protected void removeDocument(Document doc) {
1290        try {
1291            String reason = canRemoveDocument(doc);
1292            if (reason != null) {
1293                throw new DocumentSecurityException(
1294                        "Permission denied: cannot remove document " + doc.getUUID() + ", " + reason);
1295            }
1296            removeNotifyOneDoc(doc);
1297
1298        } catch (ConcurrentUpdateException e) {
1299            e.addInfo("Failed to remove document " + doc.getUUID());
1300            throw e;
1301        }
1302        deleteDocumentCount.inc();
1303    }
1304
1305    protected void removeNotifyOneDoc(Document doc) {
1306        // XXX notify with options if needed
1307        DocumentModel docModel = readModel(doc);
1308        Map<String, Serializable> options = new HashMap<String, Serializable>();
1309        if (docModel != null) {
1310            options.put("docTitle", docModel.getTitle());
1311        }
1312        String versionLabel = "";
1313        Document sourceDoc = null;
1314        // notify different events depending on wether the document is a
1315        // version or not
1316        if (!doc.isVersion()) {
1317            notifyEvent(DocumentEventTypes.ABOUT_TO_REMOVE, docModel, options, null, null, true, true);
1318            CoreService coreService = Framework.getLocalService(CoreService.class);
1319            coreService.getVersionRemovalPolicy().removeVersions(getSession(), doc, this);
1320        } else {
1321            versionLabel = docModel.getVersionLabel();
1322            sourceDoc = doc.getSourceDocument();
1323            notifyEvent(DocumentEventTypes.ABOUT_TO_REMOVE_VERSION, docModel, options, null, null, true, true);
1324
1325        }
1326        doc.remove();
1327        if (doc.isVersion()) {
1328            if (sourceDoc != null) {
1329                DocumentModel sourceDocModel = readModel(sourceDoc);
1330                if (sourceDocModel != null) {
1331                    options.put("comment", versionLabel); // to be used by
1332                                                          // audit
1333                    // service
1334                    notifyEvent(DocumentEventTypes.VERSION_REMOVED, sourceDocModel, options, null, null, false, false);
1335                    options.remove("comment");
1336                }
1337                options.put("docSource", sourceDoc.getUUID());
1338            }
1339        }
1340        notifyEvent(DocumentEventTypes.DOCUMENT_REMOVED, docModel, options, null, null, false, false);
1341    }
1342
1343    /**
1344     * Implementation uses the fact that the lexicographic ordering of paths is a refinement of the "contains" partial
1345     * ordering.
1346     */
1347    @Override
1348    public void removeDocuments(DocumentRef[] docRefs) {
1349        Document[] docs = new Document[docRefs.length];
1350
1351        for (int i = 0; i < docs.length; i++) {
1352            docs[i] = resolveReference(docRefs[i]);
1353        }
1354        // TODO OPTIM: it's not guaranteed that getPath is cheap and
1355        // we call it a lot. Should use an object for pairs (document, path)
1356        // to call it just once per doc.
1357        Arrays.sort(docs, pathComparator);
1358        String[] paths = new String[docs.length];
1359        for (int i = 0; i < docs.length; i++) {
1360            paths[i] = docs[i].getPath();
1361        }
1362        String latestRemoved = null;
1363        for (int i = 0; i < docs.length; i++) {
1364            if (i == 0 || !paths[i].startsWith(latestRemoved + "/")) {
1365                removeDocument(docs[i]);
1366                latestRemoved = paths[i];
1367            }
1368        }
1369    }
1370
1371    @Override
1372    public void save() {
1373        try {
1374            final Map<String, Serializable> options = new HashMap<String, Serializable>();
1375            getSession().save();
1376            notifyEvent(DocumentEventTypes.SESSION_SAVED, null, options, null, null, true, false);
1377        } catch (ConcurrentUpdateException e) {
1378            e.addInfo("Failed to save session");
1379            throw e;
1380        }
1381    }
1382
1383    @Override
1384    public DocumentModel saveDocument(DocumentModel docModel) {
1385        if (docModel.getRef() == null) {
1386            throw new IllegalArgumentException(String.format(
1387                    "cannot save document '%s' with null reference: " + "document has probably not yet been created "
1388                            + "in the repository with " + "'CoreSession.createDocument(docModel)'",
1389                    docModel.getTitle()));
1390        }
1391        Document doc = resolveReference(docModel.getRef());
1392        checkPermission(doc, WRITE_PROPERTIES);
1393
1394        Map<String, Serializable> options = getContextMapEventInfo(docModel);
1395
1396        boolean dirty = docModel.isDirty();
1397
1398        // document validation
1399        if (dirty && getValidationService().isActivated(DocumentValidationService.CTX_SAVEDOC, options)) {
1400            DocumentValidationReport report = getValidationService().validate(docModel, true);
1401            if (report.hasError()) {
1402                throw new DocumentValidationException(report);
1403            }
1404        }
1405
1406        options.put(CoreEventConstants.PREVIOUS_DOCUMENT_MODEL, readModel(doc));
1407        // regular event, last chance to modify docModel
1408        options.put(CoreEventConstants.DESTINATION_NAME, docModel.getName());
1409        options.put(CoreEventConstants.DOCUMENT_DIRTY, dirty);
1410        notifyEvent(DocumentEventTypes.BEFORE_DOC_UPDATE, docModel, options, null, null, true, true);
1411        String name = (String) options.get(CoreEventConstants.DESTINATION_NAME);
1412        // did the event change the name? not applicable to Root whose
1413        // name is null/empty
1414        if (name != null && !name.equals(docModel.getName())) {
1415            doc = getSession().move(doc, doc.getParent(), name);
1416        }
1417
1418        VersioningOption versioningOption = (VersioningOption) docModel.getContextData(
1419                VersioningService.VERSIONING_OPTION);
1420        docModel.putContextData(VersioningService.VERSIONING_OPTION, null);
1421        String checkinComment = (String) docModel.getContextData(VersioningService.CHECKIN_COMMENT);
1422        docModel.putContextData(VersioningService.CHECKIN_COMMENT, null);
1423        Boolean disableAutoCheckOut = (Boolean) docModel.getContextData(VersioningService.DISABLE_AUTO_CHECKOUT);
1424        docModel.putContextData(VersioningService.DISABLE_AUTO_CHECKOUT, null);
1425        options.put(VersioningService.DISABLE_AUTO_CHECKOUT, disableAutoCheckOut);
1426        // compat
1427        boolean snapshot = Boolean.TRUE.equals(
1428                docModel.getContextData(ScopeType.REQUEST, VersioningDocument.CREATE_SNAPSHOT_ON_SAVE_KEY));
1429        docModel.putContextData(ScopeType.REQUEST, VersioningDocument.CREATE_SNAPSHOT_ON_SAVE_KEY, null);
1430        if (versioningOption == null && snapshot && dirty) {
1431            String key = String.valueOf(
1432                    docModel.getContextData(ScopeType.REQUEST, VersioningDocument.KEY_FOR_INC_OPTION));
1433            docModel.putContextData(ScopeType.REQUEST, VersioningDocument.KEY_FOR_INC_OPTION, null);
1434            versioningOption = "inc_major".equals(key) ? VersioningOption.MAJOR : VersioningOption.MINOR;
1435        }
1436
1437        if (!docModel.isImmutable()) {
1438            // pre-save versioning
1439            boolean checkout = getVersioningService().isPreSaveDoingCheckOut(doc, dirty, versioningOption, options);
1440            if (checkout) {
1441                notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true);
1442            }
1443            versioningOption = getVersioningService().doPreSave(doc, dirty, versioningOption, checkinComment, options);
1444            if (checkout) {
1445                DocumentModel checkedOutDoc = readModel(doc);
1446                notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, checkedOutDoc, options, null, null, true, false);
1447            }
1448        }
1449
1450        boolean allowVersionWrite = Boolean.TRUE.equals(docModel.getContextData(ALLOW_VERSION_WRITE));
1451        docModel.putContextData(ALLOW_VERSION_WRITE, null);
1452        boolean setReadWrite = allowVersionWrite && doc.isVersion() && doc.isReadOnly();
1453
1454        // actual save
1455        if (setReadWrite) {
1456            doc.setReadOnly(false);
1457        }
1458        docModel = writeModel(doc, docModel);
1459        if (setReadWrite) {
1460            doc.setReadOnly(true);
1461        }
1462
1463        Document checkedInDoc = null;
1464        if (!docModel.isImmutable()) {
1465            // post-save versioning
1466            boolean checkin = getVersioningService().isPostSaveDoingCheckIn(doc, versioningOption, options);
1467            if (checkin) {
1468                notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true);
1469            }
1470            checkedInDoc = getVersioningService().doPostSave(doc, versioningOption, checkinComment, options);
1471        }
1472
1473        // post-save events
1474        docModel = readModel(doc);
1475        if (checkedInDoc != null) {
1476            DocumentRef checkedInVersionRef = new IdRef(checkedInDoc.getUUID());
1477            notifyCheckedInVersion(docModel, checkedInVersionRef, options, checkinComment);
1478        }
1479        notifyEvent(DocumentEventTypes.DOCUMENT_UPDATED, docModel, options, null, null, true, false);
1480        updateDocumentCount.inc();
1481        return docModel;
1482    }
1483
1484    @Override
1485    @Deprecated
1486    public boolean isDirty(DocumentRef docRef) {
1487        return resolveReference(docRef).isCheckedOut();
1488    }
1489
1490    @Override
1491    public void saveDocuments(DocumentModel[] docModels) {
1492        // TODO: optimize this - avoid calling at each iteration saveDoc...
1493        for (DocumentModel docModel : docModels) {
1494            saveDocument(docModel);
1495        }
1496    }
1497
1498    @Override
1499    public DocumentModel getSourceDocument(DocumentRef docRef) {
1500        assert null != docRef;
1501
1502        Document doc = resolveReference(docRef);
1503        checkPermission(doc, READ_VERSION);
1504        Document headDocument = doc.getSourceDocument();
1505        if (headDocument == null) {
1506            throw new DocumentNotFoundException("Source document has been deleted");
1507        }
1508        return readModel(headDocument);
1509    }
1510
1511    protected VersionModel getVersionModel(Document version) {
1512        VersionModel versionModel = new VersionModelImpl();
1513        versionModel.setId(version.getUUID());
1514        versionModel.setCreated(version.getVersionCreationDate());
1515        versionModel.setDescription(version.getCheckinComment());
1516        versionModel.setLabel(version.getVersionLabel());
1517        return versionModel;
1518    }
1519
1520    @Override
1521    public VersionModel getLastVersion(DocumentRef docRef) {
1522        Document doc = resolveReference(docRef);
1523        checkPermission(doc, READ_VERSION);
1524        Document version = doc.getLastVersion();
1525        return version == null ? null : getVersionModel(version);
1526    }
1527
1528    @Override
1529    public DocumentModel getLastDocumentVersion(DocumentRef docRef) {
1530        Document doc = resolveReference(docRef);
1531        checkPermission(doc, READ_VERSION);
1532        Document version = doc.getLastVersion();
1533        return version == null ? null : readModel(version);
1534    }
1535
1536    @Override
1537    public DocumentRef getLastDocumentVersionRef(DocumentRef docRef) {
1538        Document doc = resolveReference(docRef);
1539        checkPermission(doc, READ_VERSION);
1540        Document version = doc.getLastVersion();
1541        return version == null ? null : new IdRef(version.getUUID());
1542    }
1543
1544    @Override
1545    public List<DocumentRef> getVersionsRefs(DocumentRef docRef) {
1546        Document doc = resolveReference(docRef);
1547        checkPermission(doc, READ_VERSION);
1548        List<String> ids = doc.getVersionsIds();
1549        List<DocumentRef> refs = new ArrayList<DocumentRef>(ids.size());
1550        for (String id : ids) {
1551            refs.add(new IdRef(id));
1552        }
1553        return refs;
1554    }
1555
1556    @Override
1557    public List<DocumentModel> getVersions(DocumentRef docRef) {
1558        Document doc = resolveReference(docRef);
1559        checkPermission(doc, READ_VERSION);
1560        List<Document> docVersions = doc.getVersions();
1561        List<DocumentModel> versions = new ArrayList<DocumentModel>(docVersions.size());
1562        for (Document version : docVersions) {
1563            versions.add(readModel(version));
1564        }
1565        return versions;
1566    }
1567
1568    @Override
1569    public List<VersionModel> getVersionsForDocument(DocumentRef docRef) {
1570        Document doc = resolveReference(docRef);
1571        checkPermission(doc, READ_VERSION);
1572        List<Document> docVersions = doc.getVersions();
1573        List<VersionModel> versions = new ArrayList<VersionModel>(docVersions.size());
1574        for (Document version : docVersions) {
1575            versions.add(getVersionModel(version));
1576        }
1577        return versions;
1578
1579    }
1580
1581    @Override
1582    public DocumentModel restoreToVersion(DocumentRef docRef, DocumentRef versionRef) {
1583        Document doc = resolveReference(docRef);
1584        Document ver = resolveReference(versionRef);
1585        return restoreToVersion(doc, ver, false, true);
1586    }
1587
1588    @Override
1589    @Deprecated
1590    public DocumentModel restoreToVersion(DocumentRef docRef, VersionModel version) {
1591        return restoreToVersion(docRef, version, false);
1592    }
1593
1594    @Override
1595    @Deprecated
1596    public DocumentModel restoreToVersion(DocumentRef docRef, VersionModel version, boolean skipSnapshotCreation) {
1597        Document doc = resolveReference(docRef);
1598        Document ver = doc.getVersion(version.getLabel());
1599        return restoreToVersion(doc, ver, skipSnapshotCreation, false);
1600    }
1601
1602    @Override
1603    public DocumentModel restoreToVersion(DocumentRef docRef, DocumentRef versionRef, boolean skipSnapshotCreation,
1604            boolean skipCheckout) {
1605        Document doc = resolveReference(docRef);
1606        Document ver = resolveReference(versionRef);
1607        return restoreToVersion(doc, ver, skipSnapshotCreation, skipCheckout);
1608    }
1609
1610    protected DocumentModel restoreToVersion(Document doc, Document version, boolean skipSnapshotCreation,
1611            boolean skipCheckout) {
1612        checkPermission(doc, WRITE_VERSION);
1613
1614        DocumentModel docModel = readModel(doc);
1615
1616        Map<String, Serializable> options = new HashMap<String, Serializable>();
1617
1618        // we're about to overwrite the document, make sure it's archived
1619        if (!skipSnapshotCreation && doc.isCheckedOut()) {
1620            String checkinComment = (String) docModel.getContextData(VersioningService.CHECKIN_COMMENT);
1621            docModel.putContextData(VersioningService.CHECKIN_COMMENT, null);
1622            notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true);
1623            Document ver = getVersioningService().doCheckIn(doc, null, checkinComment);
1624            docModel.refresh(DocumentModel.REFRESH_STATE, null);
1625            notifyCheckedInVersion(docModel, new IdRef(ver.getUUID()), null, checkinComment);
1626        }
1627
1628        // FIXME: the fields are hardcoded. should be moved in versioning
1629        // component
1630        // HOW?
1631        final Long majorVer = (Long) doc.getPropertyValue("major_version");
1632        final Long minorVer = (Long) doc.getPropertyValue("minor_version");
1633        if (majorVer != null || minorVer != null) {
1634            options.put(VersioningDocument.CURRENT_DOCUMENT_MAJOR_VERSION_KEY, majorVer);
1635            options.put(VersioningDocument.CURRENT_DOCUMENT_MINOR_VERSION_KEY, minorVer);
1636        }
1637        // add the uuid of the version being restored
1638        String versionUUID = version.getUUID();
1639        options.put(VersioningDocument.RESTORED_VERSION_UUID_KEY, versionUUID);
1640
1641        notifyEvent(DocumentEventTypes.BEFORE_DOC_RESTORE, docModel, options, null, null, true, true);
1642        writeModel(doc, docModel);
1643
1644        doc.restore(version);
1645        // re-read doc model after restoration
1646        docModel = readModel(doc);
1647        notifyEvent(DocumentEventTypes.DOCUMENT_RESTORED, docModel, options, null, docModel.getVersionLabel(), true,
1648                false);
1649        docModel = writeModel(doc, docModel);
1650
1651        if (!skipCheckout) {
1652            // restore gives us a checked in document, so do a checkout
1653            notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true);
1654            getVersioningService().doCheckOut(doc);
1655            docModel = readModel(doc);
1656            notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, docModel, options, null, null, true, false);
1657        }
1658
1659        log.debug("Document restored to version:" + version.getUUID());
1660        return docModel;
1661    }
1662
1663    @Override
1664    public DocumentRef getBaseVersion(DocumentRef docRef) {
1665        Document doc = resolveReference(docRef);
1666        checkPermission(doc, READ);
1667        Document ver = doc.getBaseVersion();
1668        if (ver == null) {
1669            return null;
1670        }
1671        checkPermission(ver, READ);
1672        return new IdRef(ver.getUUID());
1673    }
1674
1675    @Override
1676    @Deprecated
1677    public DocumentModel checkIn(DocumentRef docRef, VersionModel ver) {
1678        DocumentRef verRef = checkIn(docRef, VersioningOption.MINOR, ver == null ? null : ver.getDescription());
1679        return readModel(resolveReference(verRef));
1680    }
1681
1682    @Override
1683    public DocumentRef checkIn(DocumentRef docRef, VersioningOption option, String checkinComment) {
1684        Document doc = resolveReference(docRef);
1685        checkPermission(doc, WRITE_PROPERTIES);
1686        DocumentModel docModel = readModel(doc);
1687
1688        Map<String, Serializable> options = new HashMap<String, Serializable>();
1689        notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true);
1690        writeModel(doc, docModel);
1691
1692        Document version = getVersioningService().doCheckIn(doc, option, checkinComment);
1693
1694        docModel = readModel(doc);
1695        DocumentRef checkedInVersionRef = new IdRef(version.getUUID());
1696        notifyCheckedInVersion(docModel, checkedInVersionRef, options, checkinComment);
1697        writeModel(doc, docModel);
1698
1699        return checkedInVersionRef;
1700    }
1701
1702    /**
1703     * Send a core event for the creation of a new check in version. The source document is the live document model used
1704     * as the source for the checkin, not the archived version it-self.
1705     *
1706     * @param docModel work document that has been checked-in as a version
1707     * @param checkedInVersionRef document ref of the new checked-in version
1708     * @param options initial option map, or null
1709     * @param checkinComment
1710     */
1711    protected void notifyCheckedInVersion(DocumentModel docModel, DocumentRef checkedInVersionRef,
1712            Map<String, Serializable> options, String checkinComment) {
1713        String label = getVersioningService().getVersionLabel(docModel);
1714        Map<String, Serializable> props = new HashMap<String, Serializable>();
1715        if (options != null) {
1716            props.putAll(options);
1717        }
1718        props.put("versionLabel", label);
1719        props.put("checkInComment", checkinComment);
1720        props.put("checkedInVersionRef", checkedInVersionRef);
1721        if (checkinComment == null && options != null) {
1722            // check if there's a comment already in options
1723            Object optionsComment = options.get("comment");
1724            if (optionsComment instanceof String) {
1725                checkinComment = (String) optionsComment;
1726            }
1727        }
1728        String comment = checkinComment == null ? label : label + ' ' + checkinComment;
1729        props.put("comment", comment); // compat, used in audit
1730        // notify checkin on live document
1731        notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDIN, docModel, props, null, null, true, false);
1732        // notify creation on version document
1733        notifyEvent(DocumentEventTypes.DOCUMENT_CREATED, getDocument(checkedInVersionRef), props, null, null, true,
1734                false);
1735
1736    }
1737
1738    @Override
1739    public void checkOut(DocumentRef docRef) {
1740        Document doc = resolveReference(docRef);
1741        // TODO: add a new permission names CHECKOUT and use it instead of
1742        // WRITE_PROPERTIES
1743        checkPermission(doc, WRITE_PROPERTIES);
1744        DocumentModel docModel = readModel(doc);
1745        Map<String, Serializable> options = new HashMap<String, Serializable>();
1746
1747        notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true);
1748
1749        getVersioningService().doCheckOut(doc);
1750        docModel = readModel(doc);
1751
1752        notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, docModel, options, null, null, true, false);
1753        writeModel(doc, docModel);
1754    }
1755
1756    @Override
1757    public boolean isCheckedOut(DocumentRef docRef) {
1758        assert null != docRef;
1759        Document doc = resolveReference(docRef);
1760        checkPermission(doc, BROWSE);
1761        return doc.isCheckedOut();
1762    }
1763
1764    @Override
1765    public String getVersionSeriesId(DocumentRef docRef) {
1766        Document doc = resolveReference(docRef);
1767        checkPermission(doc, READ);
1768        return doc.getVersionSeriesId();
1769    }
1770
1771    @Override
1772    public DocumentModel getWorkingCopy(DocumentRef docRef) {
1773        Document doc = resolveReference(docRef);
1774        checkPermission(doc, READ_VERSION);
1775        Document pwc = doc.getWorkingCopy();
1776        checkPermission(pwc, READ);
1777        return pwc == null ? null : readModel(pwc);
1778    }
1779
1780    @Override
1781    public DocumentModel getVersion(String versionableId, VersionModel versionModel) {
1782        String id = versionModel.getId();
1783        if (id != null) {
1784            return getDocument(new IdRef(id));
1785        }
1786        Document doc = getSession().getVersion(versionableId, versionModel);
1787        if (doc == null) {
1788            return null;
1789        }
1790        checkPermission(doc, READ_PROPERTIES);
1791        checkPermission(doc, READ_VERSION);
1792        return readModel(doc);
1793    }
1794
1795    @Override
1796    public String getVersionLabel(DocumentModel docModel) {
1797        return getVersioningService().getVersionLabel(docModel);
1798    }
1799
1800    @Override
1801    public DocumentModel getDocumentWithVersion(DocumentRef docRef, VersionModel version) {
1802        String id = version.getId();
1803        if (id != null) {
1804            return getDocument(new IdRef(id));
1805        }
1806        Document doc = resolveReference(docRef);
1807        checkPermission(doc, READ_PROPERTIES);
1808        checkPermission(doc, READ_VERSION);
1809        String docPath = doc.getPath();
1810        doc = doc.getVersion(version.getLabel());
1811        if (doc == null) {
1812            // SQL Storage uses to return null if version not found
1813            log.debug("Version " + version.getLabel() + " does not exist for " + docPath);
1814            return null;
1815        }
1816        log.debug("Retrieved the version " + version.getLabel() + " of the document " + docPath);
1817        return readModel(doc);
1818    }
1819
1820    @Override
1821    public DocumentModel createProxy(DocumentRef docRef, DocumentRef folderRef) {
1822        Document doc = resolveReference(docRef);
1823        Document fold = resolveReference(folderRef);
1824        checkPermission(doc, READ);
1825        checkPermission(fold, ADD_CHILDREN);
1826        return createProxyInternal(doc, fold, new HashMap<String, Serializable>());
1827    }
1828
1829    protected DocumentModel createProxyInternal(Document doc, Document folder, Map<String, Serializable> options) {
1830        // create the new proxy
1831        Document proxy = getSession().createProxy(doc, folder);
1832        DocumentModel proxyModel = readModel(proxy);
1833
1834        notifyEvent(DocumentEventTypes.DOCUMENT_CREATED, proxyModel, options, null, null, true, false);
1835        notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_PUBLISHED, proxyModel, options, null, null, true, false);
1836        DocumentModel folderModel = readModel(folder);
1837        notifyEvent(DocumentEventTypes.SECTION_CONTENT_PUBLISHED, folderModel, options, null, null, true, false);
1838        return proxyModel;
1839    }
1840
1841    /**
1842     * Remove proxies for the same base document in the folder. doc may be a normal document or a proxy.
1843     */
1844    protected List<String> removeExistingProxies(Document doc, Document folder) {
1845        Collection<Document> otherProxies = getSession().getProxies(doc, folder);
1846        List<String> removedProxyIds = new ArrayList<String>(otherProxies.size());
1847        for (Document otherProxy : otherProxies) {
1848            removedProxyIds.add(otherProxy.getUUID());
1849            removeNotifyOneDoc(otherProxy);
1850        }
1851        return removedProxyIds;
1852    }
1853
1854    /**
1855     * Update the proxy for doc in the given section to point to the new target. Do nothing if there are several
1856     * proxies.
1857     *
1858     * @return the proxy if it was updated, or {@code null} if none or several were found
1859     */
1860    protected DocumentModel updateExistingProxies(Document doc, Document folder, Document target) {
1861        Collection<Document> proxies = getSession().getProxies(doc, folder);
1862        try {
1863            if (proxies.size() == 1) {
1864                for (Document proxy : proxies) {
1865                    proxy.setTargetDocument(target);
1866                    return readModel(proxy);
1867                }
1868            }
1869        } catch (UnsupportedOperationException e) {
1870            log.error("Cannot update proxy, try to remove");
1871        }
1872        return null;
1873    }
1874
1875    @Override
1876    public DocumentModelList getProxies(DocumentRef docRef, DocumentRef folderRef) {
1877        Document folder = null;
1878        if (folderRef != null) {
1879            folder = resolveReference(folderRef);
1880            checkPermission(folder, READ_CHILDREN);
1881        }
1882        Document doc = resolveReference(docRef);
1883        Collection<Document> children = getSession().getProxies(doc, folder);
1884        DocumentModelList docs = new DocumentModelListImpl();
1885        for (Document child : children) {
1886            if (hasPermission(child, READ)) {
1887                docs.add(readModel(child));
1888            }
1889        }
1890        return docs;
1891    }
1892
1893    @Override
1894    public String[] getProxyVersions(DocumentRef docRef, DocumentRef folderRef) {
1895        Document folder = resolveReference(folderRef);
1896        Document doc = resolveReference(docRef);
1897        checkPermission(folder, READ_CHILDREN);
1898        Collection<Document> children = getSession().getProxies(doc, folder);
1899        if (children.isEmpty()) {
1900            return null;
1901        }
1902        List<String> versions = new ArrayList<String>();
1903        for (Document child : children) {
1904            if (hasPermission(child, READ)) {
1905                Document target = child.getTargetDocument();
1906                if (target.isVersion()) {
1907                    versions.add(target.getVersionLabel());
1908                } else {
1909                    // live proxy
1910                    versions.add("");
1911                }
1912            }
1913        }
1914        return versions.toArray(new String[versions.size()]);
1915    }
1916
1917    @Override
1918    public List<String> getAvailableSecurityPermissions() {
1919        // XXX: add security check?
1920        return Arrays.asList(getSecurityService().getPermissionProvider().getPermissions());
1921    }
1922
1923    @Override
1924    public DataModel getDataModel(DocumentRef docRef, Schema schema) {
1925        Document doc = resolveReference(docRef);
1926        checkPermission(doc, READ);
1927        return DocumentModelFactory.createDataModel(doc, schema);
1928    }
1929
1930    protected Object getDataModelField(DocumentRef docRef, String schema, String field) {
1931        Document doc = resolveReference(docRef);
1932        if (doc != null) {
1933            checkPermission(doc, READ);
1934            Schema docSchema = doc.getType().getSchema(schema);
1935            if (docSchema != null) {
1936                String prefix = docSchema.getNamespace().prefix;
1937                if (prefix != null && prefix.length() > 0) {
1938                    field = prefix + ':' + field;
1939                }
1940                return doc.getPropertyValue(field);
1941            } else {
1942                log.warn("Cannot find schema with name=" + schema);
1943            }
1944        } else {
1945            log.warn("Cannot resolve docRef=" + docRef);
1946        }
1947        return null;
1948    }
1949
1950    @Override
1951    public String getCurrentLifeCycleState(DocumentRef docRef) {
1952        Document doc = resolveReference(docRef);
1953        checkPermission(doc, READ_LIFE_CYCLE);
1954        return doc.getLifeCycleState();
1955    }
1956
1957    @Override
1958    public String getLifeCyclePolicy(DocumentRef docRef) {
1959        Document doc = resolveReference(docRef);
1960        checkPermission(doc, READ_LIFE_CYCLE);
1961        return doc.getLifeCyclePolicy();
1962    }
1963
1964    /**
1965     * Make a document follow a transition.
1966     *
1967     * @param docRef a {@link DocumentRef}
1968     * @param transition the transition to follow
1969     * @param options an option map than can be used by callers to pass additional params
1970     * @return
1971     * @since 5.9.3
1972     */
1973    private boolean followTransition(DocumentRef docRef, String transition, ScopedMap options)
1974            throws LifeCycleException {
1975        Document doc = resolveReference(docRef);
1976        checkPermission(doc, WRITE_LIFE_CYCLE);
1977
1978        if (!doc.isVersion() && !doc.isProxy() && !doc.isCheckedOut()) {
1979            checkOut(docRef);
1980            doc = resolveReference(docRef);
1981        }
1982        String formerStateName = doc.getLifeCycleState();
1983        doc.followTransition(transition);
1984
1985        // Construct a map holding meta information about the event.
1986        Map<String, Serializable> eventOptions = new HashMap<String, Serializable>();
1987        eventOptions.put(org.nuxeo.ecm.core.api.LifeCycleConstants.TRANSTION_EVENT_OPTION_FROM, formerStateName);
1988        eventOptions.put(org.nuxeo.ecm.core.api.LifeCycleConstants.TRANSTION_EVENT_OPTION_TO, doc.getLifeCycleState());
1989        eventOptions.put(org.nuxeo.ecm.core.api.LifeCycleConstants.TRANSTION_EVENT_OPTION_TRANSITION, transition);
1990        String comment = (String) options.getScopedValue("comment");
1991        DocumentModel docModel = readModel(doc);
1992        notifyEvent(org.nuxeo.ecm.core.api.LifeCycleConstants.TRANSITION_EVENT, docModel, eventOptions,
1993                DocumentEventCategories.EVENT_LIFE_CYCLE_CATEGORY, comment, true, false);
1994        if (!docModel.isImmutable()) {
1995            writeModel(doc, docModel);
1996        }
1997        return true; // throws if error
1998    }
1999
2000    @Override
2001    public boolean followTransition(DocumentModel docModel, String transition) throws LifeCycleException {
2002        return followTransition(docModel.getRef(), transition, docModel.getContextData());
2003    }
2004
2005    @Override
2006    public boolean followTransition(DocumentRef docRef, String transition) throws LifeCycleException {
2007        return followTransition(docRef, transition, new ScopedMap());
2008    }
2009
2010    @Override
2011    public Collection<String> getAllowedStateTransitions(DocumentRef docRef) {
2012        Document doc = resolveReference(docRef);
2013        checkPermission(doc, READ_LIFE_CYCLE);
2014        return doc.getAllowedStateTransitions();
2015    }
2016
2017    @Override
2018    public void reinitLifeCycleState(DocumentRef docRef) {
2019        Document doc = resolveReference(docRef);
2020        checkPermission(doc, WRITE_LIFE_CYCLE);
2021        LifeCycleService service = NXCore.getLifeCycleService();
2022        service.reinitLifeCycle(doc);
2023    }
2024
2025    @Override
2026    public Object[] getDataModelsField(DocumentRef[] docRefs, String schema, String field) {
2027
2028        assert docRefs != null;
2029        assert schema != null;
2030        assert field != null;
2031
2032        final Object[] values = new Object[docRefs.length];
2033        int i = 0;
2034        for (DocumentRef docRef : docRefs) {
2035            final Object value = getDataModelField(docRef, schema, field);
2036            values[i++] = value;
2037        }
2038
2039        return values;
2040    }
2041
2042    @Override
2043    public DocumentRef[] getParentDocumentRefs(DocumentRef docRef) {
2044        final List<DocumentRef> docRefs = new ArrayList<DocumentRef>();
2045        final Document doc = resolveReference(docRef);
2046        Document parentDoc = doc.getParent();
2047        while (parentDoc != null) {
2048            final DocumentRef parentDocRef = new IdRef(parentDoc.getUUID());
2049            docRefs.add(parentDocRef);
2050            parentDoc = parentDoc.getParent();
2051        }
2052        DocumentRef[] refs = new DocumentRef[docRefs.size()];
2053        return docRefs.toArray(refs);
2054    }
2055
2056    @Override
2057    public Object[] getDataModelsFieldUp(DocumentRef docRef, String schema, String field) {
2058
2059        final DocumentRef[] parentRefs = getParentDocumentRefs(docRef);
2060        final DocumentRef[] allRefs = new DocumentRef[parentRefs.length + 1];
2061        allRefs[0] = docRef;
2062        System.arraycopy(parentRefs, 0, allRefs, 1, parentRefs.length);
2063
2064        return getDataModelsField(allRefs, schema, field);
2065    }
2066
2067    protected String oldLockKey(Lock lock) {
2068        if (lock == null) {
2069            return null;
2070        }
2071        // return deprecated format, like "someuser:Nov 29, 2010"
2072        String lockCreationDate = (lock.getCreated() == null) ? null
2073                : DateFormat.getDateInstance(DateFormat.MEDIUM).format(new Date(lock.getCreated().getTimeInMillis()));
2074        return lock.getOwner() + ':' + lockCreationDate;
2075    }
2076
2077    @Override
2078    @Deprecated
2079    public String getLock(DocumentRef docRef) {
2080        Lock lock = getLockInfo(docRef);
2081        return oldLockKey(lock);
2082    }
2083
2084    @Override
2085    @Deprecated
2086    public void setLock(DocumentRef docRef, String key) {
2087        setLock(docRef);
2088    }
2089
2090    @Override
2091    @Deprecated
2092    public String unlock(DocumentRef docRef) {
2093        Lock lock = removeLock(docRef);
2094        return oldLockKey(lock);
2095    }
2096
2097    @Override
2098    public Lock setLock(DocumentRef docRef) throws LockException {
2099        Document doc = resolveReference(docRef);
2100        // TODO: add a new permission named LOCK and use it instead of
2101        // WRITE_PROPERTIES
2102        checkPermission(doc, WRITE_PROPERTIES);
2103        Lock lock = new Lock(getPrincipal().getName(), new GregorianCalendar());
2104        Lock oldLock = doc.setLock(lock);
2105        if (oldLock != null) {
2106            throw new LockException("Document already locked by " + oldLock.getOwner() + ": " + docRef);
2107        }
2108        DocumentModel docModel = readModel(doc);
2109        Map<String, Serializable> options = new HashMap<String, Serializable>();
2110        options.put("lock", lock);
2111        notifyEvent(DocumentEventTypes.DOCUMENT_LOCKED, docModel, options, null, null, true, false);
2112        return lock;
2113    }
2114
2115    @Override
2116    public Lock getLockInfo(DocumentRef docRef) {
2117        Document doc = resolveReference(docRef);
2118        checkPermission(doc, READ);
2119        return doc.getLock();
2120    }
2121
2122    @Override
2123    public Lock removeLock(DocumentRef docRef) throws LockException {
2124        Document doc = resolveReference(docRef);
2125        String owner;
2126        if (hasPermission(docRef, UNLOCK)) {
2127            // always unlock
2128            owner = null;
2129        } else {
2130            owner = getPrincipal().getName();
2131        }
2132        Lock lock = doc.removeLock(owner);
2133        if (lock == null) {
2134            // there was no lock, we're done
2135            return null;
2136        }
2137        if (lock.getFailed()) {
2138            // lock removal failed due to owner check
2139            throw new LockException("Document already locked by " + lock.getOwner() + ": " + docRef);
2140        }
2141        DocumentModel docModel = readModel(doc);
2142        Map<String, Serializable> options = new HashMap<String, Serializable>();
2143        options.put("lock", lock);
2144        notifyEvent(DocumentEventTypes.DOCUMENT_UNLOCKED, docModel, options, null, null, true, false);
2145        return lock;
2146    }
2147
2148    protected boolean isAdministrator() {
2149        Principal principal = getPrincipal();
2150        // FIXME: this is inconsistent with NuxeoPrincipal#isAdministrator
2151        // method because it allows hardcoded Administrator user
2152        if (Framework.isTestModeSet()) {
2153            if (SecurityConstants.ADMINISTRATOR.equals(principal.getName())) {
2154                return true;
2155            }
2156        }
2157        if (SYSTEM_USERNAME.equals(principal.getName())) {
2158            return true;
2159        }
2160        if (principal instanceof NuxeoPrincipal) {
2161            return ((NuxeoPrincipal) principal).isAdministrator();
2162        }
2163        return false;
2164    }
2165
2166    @Override
2167    public void applyDefaultPermissions(String userOrGroupName) {
2168        if (userOrGroupName == null) {
2169            throw new NullPointerException("null userOrGroupName");
2170        }
2171        if (!isAdministrator()) {
2172            throw new DocumentSecurityException("You need to be an Administrator to do this.");
2173        }
2174        DocumentModel rootDocument = getRootDocument();
2175        ACP acp = new ACPImpl();
2176
2177        UserEntry userEntry = new UserEntryImpl(userOrGroupName);
2178        userEntry.addPrivilege(READ);
2179
2180        acp.setRules(new UserEntry[] { userEntry });
2181
2182        setACP(rootDocument.getRef(), acp, false);
2183    }
2184
2185    @Override
2186    public DocumentModel publishDocument(DocumentModel docToPublish, DocumentModel section) {
2187        return publishDocument(docToPublish, section, true);
2188    }
2189
2190    @Override
2191    public DocumentModel publishDocument(DocumentModel docModel, DocumentModel section,
2192            boolean overwriteExistingProxy) {
2193        Document doc = resolveReference(docModel.getRef());
2194        Document sec = resolveReference(section.getRef());
2195        checkPermission(doc, READ);
2196        checkPermission(sec, ADD_CHILDREN);
2197
2198        Map<String, Serializable> options = new HashMap<String, Serializable>();
2199        DocumentModel proxy = null;
2200        Document target;
2201        if (docModel.isProxy() || docModel.isVersion()) {
2202            target = doc;
2203            if (overwriteExistingProxy) {
2204                if (docModel.isVersion()) {
2205                    Document base = resolveReference(new IdRef(doc.getVersionSeriesId()));
2206                    proxy = updateExistingProxies(base, sec, target);
2207                }
2208                if (proxy == null) {
2209                    // remove previous
2210                    List<String> removedProxyIds = removeExistingProxies(doc, sec);
2211                    options.put(CoreEventConstants.REPLACED_PROXY_IDS, (Serializable) removedProxyIds);
2212                }
2213            }
2214
2215        } else {
2216            String checkinComment = (String) docModel.getContextData(VersioningService.CHECKIN_COMMENT);
2217            docModel.putContextData(VersioningService.CHECKIN_COMMENT, null);
2218            if (doc.isCheckedOut() || doc.getLastVersion() == null) {
2219                if (!doc.isCheckedOut()) {
2220                    // last version was deleted while leaving a checked in
2221                    // doc. recreate a version
2222                    notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true);
2223                    getVersioningService().doCheckOut(doc);
2224                    docModel = readModel(doc);
2225                    notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, docModel, options, null, null, true, false);
2226                }
2227                notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true);
2228                Document version = getVersioningService().doCheckIn(doc, null, checkinComment);
2229                docModel.refresh(DocumentModel.REFRESH_STATE | DocumentModel.REFRESH_CONTENT_LAZY, null);
2230                notifyCheckedInVersion(docModel, new IdRef(version.getUUID()), null, checkinComment);
2231            }
2232            // NXP-12921: use base version because we could need to publish
2233            // a previous version (after restoring for example)
2234            target = doc.getBaseVersion();
2235            if (overwriteExistingProxy) {
2236                proxy = updateExistingProxies(doc, sec, target);
2237                if (proxy == null) {
2238                    // no or several proxies, remove them
2239                    List<String> removedProxyIds = removeExistingProxies(doc, sec);
2240                    options.put(CoreEventConstants.REPLACED_PROXY_IDS, (Serializable) removedProxyIds);
2241                } else {
2242                    // notify proxy updates
2243                    notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_UPDATED, proxy, options, null, null, true, false);
2244                    notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_PUBLISHED, proxy, options, null, null, true, false);
2245                    notifyEvent(DocumentEventTypes.SECTION_CONTENT_PUBLISHED, section, options, null, null, true,
2246                            false);
2247                }
2248            }
2249        }
2250        if (proxy == null) {
2251            proxy = createProxyInternal(target, sec, options);
2252        }
2253        return proxy;
2254    }
2255
2256    @Override
2257    public String getSuperParentType(DocumentModel doc) {
2258        DocumentModel superSpace = getSuperSpace(doc);
2259        if (superSpace == null) {
2260            return null;
2261        } else {
2262            return superSpace.getType();
2263        }
2264    }
2265
2266    @Override
2267    public DocumentModel getSuperSpace(DocumentModel doc) {
2268        if (doc == null) {
2269            throw new IllegalArgumentException("null document");
2270        }
2271        if (doc.hasFacet(FacetNames.SUPER_SPACE)) {
2272            return doc;
2273        } else {
2274
2275            DocumentModel parent = getDirectAccessibleParent(doc.getRef());
2276            if (parent == null || "/".equals(parent.getPathAsString())) {
2277                // return Root instead of null
2278                return getRootDocument();
2279            } else {
2280                return getSuperSpace(parent);
2281            }
2282        }
2283    }
2284
2285    // walk the tree up until a accessible doc is found
2286    private DocumentModel getDirectAccessibleParent(DocumentRef docRef) {
2287        Document doc = resolveReference(docRef);
2288        Document parentDoc = doc.getParent();
2289        if (parentDoc == null) {
2290            return readModel(doc);
2291        }
2292        if (!hasPermission(parentDoc, READ)) {
2293            String parentPath = parentDoc.getPath();
2294            if ("/".equals(parentPath)) {
2295                return getRootDocument();
2296            } else {
2297                // try on parent
2298                return getDirectAccessibleParent(new PathRef(parentDoc.getPath()));
2299            }
2300        }
2301        return readModel(parentDoc);
2302    }
2303
2304    @Override
2305    public <T extends Serializable> T getDocumentSystemProp(DocumentRef ref, String systemProperty, Class<T> type) {
2306        Document doc = resolveReference(ref);
2307        return doc.getSystemProp(systemProperty, type);
2308    }
2309
2310    @Override
2311    public <T extends Serializable> void setDocumentSystemProp(DocumentRef ref, String systemProperty, T value) {
2312        Document doc = resolveReference(ref);
2313        if (systemProperty != null && systemProperty.startsWith(BINARY_TEXT_SYS_PROP)) {
2314            DocumentModel docModel = readModel(doc);
2315            Map<String, Serializable> options = new HashMap<String, Serializable>();
2316            options.put(systemProperty, value != null);
2317            notifyEvent(DocumentEventTypes.BINARYTEXT_UPDATED, docModel, options, null, null, false, true);
2318        }
2319        doc.setSystemProp(systemProperty, value);
2320    }
2321
2322    @Override
2323    public void orderBefore(DocumentRef parent, String src, String dest) {
2324        if ((src == null) || (src.equals(dest))) {
2325            return;
2326        }
2327        Document doc = resolveReference(parent);
2328        doc.orderBefore(src, dest);
2329        Map<String, Serializable> options = new HashMap<String, Serializable>();
2330
2331        // send event on container passing the reordered child as parameter
2332        DocumentModel docModel = readModel(doc);
2333        String comment = src;
2334        options.put(CoreEventConstants.REORDERED_CHILD, src);
2335        notifyEvent(DocumentEventTypes.DOCUMENT_CHILDREN_ORDER_CHANGED, docModel, options, null, comment, true, false);
2336    }
2337
2338    @Override
2339    public DocumentModelRefresh refreshDocument(DocumentRef ref, int refreshFlags, String[] schemas) {
2340        Document doc = resolveReference(ref);
2341
2342        // permission checks
2343        if ((refreshFlags & (DocumentModel.REFRESH_PREFETCH | DocumentModel.REFRESH_STATE
2344                | DocumentModel.REFRESH_CONTENT)) != 0) {
2345            checkPermission(doc, READ);
2346        }
2347        if ((refreshFlags & DocumentModel.REFRESH_ACP) != 0) {
2348            checkPermission(doc, READ_SECURITY);
2349        }
2350
2351        DocumentModelRefresh refresh = DocumentModelFactory.refreshDocumentModel(doc, refreshFlags, schemas);
2352
2353        // ACPs need the session, so aren't done in the factory method
2354        if ((refreshFlags & DocumentModel.REFRESH_ACP) != 0) {
2355            refresh.acp = getSession().getMergedACP(doc);
2356        }
2357
2358        return refresh;
2359    }
2360
2361    @Override
2362    public String[] getPermissionsToCheck(String permission) {
2363        return getSecurityService().getPermissionsToCheck(permission);
2364    }
2365
2366    @Override
2367    public <T extends DetachedAdapter> T adaptFirstMatchingDocumentWithFacet(DocumentRef docRef, String facet,
2368            Class<T> adapterClass) {
2369        Document doc = getFirstParentDocumentWithFacet(docRef, facet);
2370        if (doc != null) {
2371            DocumentModel docModel = readModel(doc);
2372            loadDataModelsForFacet(docModel, doc, facet);
2373            docModel.detach(false);
2374            return docModel.getAdapter(adapterClass);
2375        }
2376        return null;
2377    }
2378
2379    protected void loadDataModelsForFacet(DocumentModel docModel, Document doc, String facetName) {
2380        // Load all the data related to facet's schemas
2381        SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
2382        CompositeType facet = schemaManager.getFacet(facetName);
2383        if (facet == null) {
2384            return;
2385        }
2386
2387        String[] facetSchemas = facet.getSchemaNames();
2388        for (String schema : facetSchemas) {
2389            DataModel dm = DocumentModelFactory.createDataModel(doc, schemaManager.getSchema(schema));
2390            docModel.getDataModels().put(schema, dm);
2391        }
2392    }
2393
2394    /**
2395     * Returns the first {@code Document} with the given {@code facet}, recursively going up the parent hierarchy.
2396     * Returns {@code null} if there is no more parent.
2397     * <p>
2398     * This method does not check security rights.
2399     */
2400    protected Document getFirstParentDocumentWithFacet(DocumentRef docRef, String facet) {
2401        Document doc = resolveReference(docRef);
2402        while (doc != null && !doc.hasFacet(facet)) {
2403            doc = doc.getParent();
2404        }
2405        return doc;
2406    }
2407
2408    @Override
2409    public Map<String, String> getBinaryFulltext(DocumentRef ref) {
2410        Document doc = resolveReference(ref);
2411        checkPermission(doc, READ);
2412        return getSession().getBinaryFulltext(doc.getUUID());
2413    }
2414
2415}