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