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