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