001/*
002 * (C) Copyright 2020 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 */
017package org.nuxeo.drive.mongodb;
018
019import static com.mongodb.client.model.Filters.and;
020import static com.mongodb.client.model.Filters.eq;
021import static com.mongodb.client.model.Filters.exists;
022import static com.mongodb.client.model.Filters.gt;
023import static com.mongodb.client.model.Filters.in;
024import static com.mongodb.client.model.Filters.lte;
025import static com.mongodb.client.model.Filters.ne;
026import static com.mongodb.client.model.Filters.or;
027import static com.mongodb.client.model.Filters.regex;
028import static com.mongodb.client.model.Sorts.ascending;
029import static com.mongodb.client.model.Sorts.descending;
030import static com.mongodb.client.model.Sorts.orderBy;
031import static org.nuxeo.ecm.platform.audit.api.BuiltinLogEntryData.LOG_ID;
032
033import java.util.ArrayList;
034import java.util.Date;
035import java.util.List;
036import java.util.Set;
037
038import org.apache.logging.log4j.LogManager;
039import org.apache.logging.log4j.Logger;
040import org.bson.Document;
041import org.bson.conversions.Bson;
042import org.nuxeo.drive.service.SynchronizationRoots;
043import org.nuxeo.drive.service.impl.AuditChangeFinder;
044import org.nuxeo.ecm.core.api.CoreSession;
045import org.nuxeo.ecm.core.query.sql.model.OrderByExprs;
046import org.nuxeo.ecm.core.query.sql.model.Predicates;
047import org.nuxeo.ecm.core.query.sql.model.QueryBuilder;
048import org.nuxeo.ecm.platform.audit.api.AuditQueryBuilder;
049import org.nuxeo.ecm.platform.audit.api.AuditReader;
050import org.nuxeo.ecm.platform.audit.api.LogEntry;
051import org.nuxeo.mongodb.audit.MongoDBAuditBackend;
052import org.nuxeo.mongodb.audit.MongoDBAuditEntryReader;
053import org.nuxeo.runtime.api.Framework;
054
055import com.mongodb.client.MongoCollection;
056
057/** @since 11.3 */
058public class MongoDBAuditChangeFinder extends AuditChangeFinder {
059
060    private static final Logger log = LogManager.getLogger(MongoDBAuditChangeFinder.class);
061
062    @Override
063    public long getUpperBound() {
064        MongoDBAuditBackend auditService = (MongoDBAuditBackend) Framework.getService(AuditReader.class);
065        // TODO remove this dummy predicate once we can query with no predicate at all
066        QueryBuilder queryBuilder = new AuditQueryBuilder().predicate(Predicates.gt("eventDate", new Date(0)));
067        queryBuilder.order(OrderByExprs.desc(LOG_ID));
068        queryBuilder.limit(1);
069        List<LogEntry> entries = auditService.queryLogs(queryBuilder);
070        if (entries.isEmpty()) {
071            log.debug("Found no audit log entries, returning -1");
072            return -1;
073        }
074        return entries.get(0).getId();
075    }
076
077    @Override
078    protected List<LogEntry> queryAuditEntries(CoreSession session, SynchronizationRoots activeRoots,
079            Set<String> collectionSyncRootMemberIds, long lowerBound, long upperBound, int limit) {
080        MongoDBAuditBackend auditService = (MongoDBAuditBackend) Framework.getService(AuditReader.class);
081        MongoCollection<Document> auditCollection = auditService.getAuditCollection();
082
083        // Build intermediate filters
084        Bson lifeCycleEvent = and(eq("category", "eventLifeCycleCategory"), eq("eventId", "lifecycle_transition_event"),
085                ne("docLifeCycle", "deleted"));
086        Bson documentEvent = and(eq("category", "eventDocumentCategory"),
087                in("eventId", "documentCreated", "documentModified", "documentMoved", "documentCreatedByCopy",
088                        "documentRestored", "addedToCollection", "documentProxyPublished", "documentLocked",
089                        "documentUnlocked", "documentUntrashed"));
090        Bson sessionRepository = eq("repositoryId", session.getRepositoryName());
091        Bson driveCategory = eq("category", "NuxeoDrive");
092        Bson notRootUnregistered = ne("eventId", "rootUnregistered");
093        Bson inEvents = or(documentEvent, lifeCycleEvent);
094        Bson isGeneral = addRoots(inEvents, activeRoots.getPaths(), collectionSyncRootMemberIds);
095        Bson isDrive = and(driveCategory, notRootUnregistered);
096        Bson idRange = and(gt("_id", lowerBound), lte("_id", upperBound));
097        Bson isUser = or(exists("extended.impactedUserName", false),
098                eq("extended.impactedUserName", session.getPrincipal().getName()));
099
100        // Build final filter
101        Bson filter = and(sessionRepository, or(isGeneral, isDrive), idRange, isUser);
102        log.debug("Query on MongoDB-Audit: {}",
103                () -> filter.toBsonDocument(Document.class, auditCollection.getCodecRegistry()).toJson());
104        Bson order = orderBy(ascending("repositoryId"), descending("eventDate"));
105
106        return auditCollection.find(filter).sort(order).map(MongoDBAuditEntryReader::read).into(new ArrayList<>());
107    }
108
109    protected Bson addRoots(Bson baseFilter, Set<String> rootPaths, Set<String> collectionSyncRootMemberIds) {
110        List<Bson> rootFilters = new ArrayList<>();
111        if (!rootPaths.isEmpty()) {
112            rootFilters.add(regex("docPath", "^" + String.join("|^", rootPaths)));
113        }
114        if (!collectionSyncRootMemberIds.isEmpty()) {
115            rootFilters.add(in("docUUID", collectionSyncRootMemberIds));
116        }
117        if (rootFilters.isEmpty()) {
118            return baseFilter;
119        } else {
120            return and(baseFilter, or(rootFilters));
121        }
122    }
123
124}