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