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