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