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