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