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