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