001/*
002 * (C) Copyright 2017-2018 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 *     Kevin Leturc
018 */
019package org.nuxeo.mongodb.audit.pageprovider;
020
021import java.io.Serializable;
022import java.util.ArrayList;
023import java.util.Calendar;
024import java.util.List;
025
026import org.apache.commons.lang3.StringUtils;
027import org.apache.logging.log4j.LogManager;
028import org.apache.logging.log4j.Logger;
029import org.bson.BsonDocument;
030import org.bson.Document;
031import org.bson.conversions.Bson;
032import org.nuxeo.ecm.core.api.CoreSession;
033import org.nuxeo.ecm.core.api.DocumentModel;
034import org.nuxeo.ecm.core.api.NuxeoException;
035import org.nuxeo.ecm.core.api.SortInfo;
036import org.nuxeo.ecm.platform.audit.api.LogEntry;
037import org.nuxeo.ecm.platform.audit.api.comment.CommentProcessorHelper;
038import org.nuxeo.ecm.platform.audit.service.AuditBackend;
039import org.nuxeo.ecm.platform.audit.service.NXAuditEventsService;
040import org.nuxeo.ecm.platform.query.api.AbstractPageProvider;
041import org.nuxeo.ecm.platform.query.api.PageProvider;
042import org.nuxeo.ecm.platform.query.api.PageProviderDefinition;
043import org.nuxeo.ecm.platform.query.api.PredicateDefinition;
044import org.nuxeo.ecm.platform.query.api.PredicateFieldDefinition;
045import org.nuxeo.ecm.platform.query.api.QuickFilter;
046import org.nuxeo.ecm.platform.query.api.WhereClauseDefinition;
047import org.nuxeo.ecm.platform.query.nxql.NXQLQueryBuilder;
048import org.nuxeo.mongodb.audit.MongoDBAuditBackend;
049import org.nuxeo.mongodb.audit.MongoDBAuditEntryReader;
050import org.nuxeo.runtime.api.Framework;
051import org.nuxeo.runtime.mongodb.MongoDBSerializationHelper;
052
053import com.mongodb.client.FindIterable;
054import com.mongodb.client.MongoCollection;
055import com.mongodb.client.model.Filters;
056import com.mongodb.client.model.Sorts;
057
058/**
059 * @since 9.1
060 */
061public class MongoDBAuditPageProvider extends AbstractPageProvider<LogEntry> implements PageProvider<LogEntry> {
062
063    private static final long serialVersionUID = 1L;
064
065    private static final Logger log = LogManager.getLogger(MongoDBAuditPageProvider.class);
066
067    private static final String EMPTY_QUERY = "{}";
068
069    public static final String CORE_SESSION_PROPERTY = "coreSession";
070
071    public static final String UICOMMENTS_PROPERTY = "generateUIComments";
072
073    @Override
074    public String toString() {
075        return buildAuditFilter().toString();
076    }
077
078    protected CoreSession getCoreSession() {
079        Object session = getProperties().get(CORE_SESSION_PROPERTY);
080        if (session instanceof CoreSession) {
081            return (CoreSession) session;
082        }
083        return null;
084    }
085
086    protected void preprocessCommentsIfNeeded(List<LogEntry> entries) {
087        Serializable preprocess = getProperties().get(UICOMMENTS_PROPERTY);
088
089        if (preprocess != null && "true".equalsIgnoreCase(preprocess.toString())) {
090            CoreSession session = getCoreSession();
091            if (session != null) {
092                CommentProcessorHelper cph = new CommentProcessorHelper(session);
093                cph.processComments(entries);
094            }
095        }
096    }
097
098    @Override
099    public List<LogEntry> getCurrentPage() {
100        long t0 = System.currentTimeMillis();
101
102        Bson filter = buildAuditFilter();
103
104        List<SortInfo> sortInfos = getSortInfos();
105        List<Bson> sorts = new ArrayList<>(sortInfos.size());
106        for (SortInfo sortInfo : sortInfos) {
107            String sortColumn = sortInfo.getSortColumn();
108            if ("id".equals(sortColumn)) {
109                sortColumn = MongoDBSerializationHelper.MONGODB_ID;
110            }
111            if (sortInfo.getSortAscending()) {
112                sorts.add(Sorts.ascending(sortColumn));
113            } else {
114                sorts.add(Sorts.descending(sortColumn));
115            }
116        }
117        MongoCollection<Document> auditCollection = getMongoDBBackend().getAuditCollection();
118        FindIterable<Document> response = auditCollection.find(filter)
119                                                         .sort(Sorts.orderBy(sorts))
120                                                         .skip((int) (getCurrentPageIndex() * pageSize))
121                                                         .limit((int) getMinMaxPageSize());
122        List<LogEntry> entries = new ArrayList<>();
123
124        // set total number of results
125        setResultsCount(auditCollection.countDocuments(filter));
126
127        for (Document document : response) {
128            entries.add(MongoDBAuditEntryReader.read(document));
129        }
130        preprocessCommentsIfNeeded(entries);
131
132        CoreSession session = getCoreSession();
133        if (session != null) {
134            // send event for statistics !
135            fireSearchEvent(session.getPrincipal(), filter.toString(), entries, System.currentTimeMillis() - t0);
136        }
137
138        return entries;
139    }
140
141    protected String getFixedPart() {
142        WhereClauseDefinition whereClause = getDefinition().getWhereClause();
143        if (whereClause == null) {
144            return null;
145        } else {
146            String fixedPart = whereClause.getFixedPart();
147            if (fixedPart == null || fixedPart.isEmpty()) {
148                fixedPart = EMPTY_QUERY;
149            }
150            return fixedPart;
151        }
152    }
153
154    protected boolean allowSimplePattern() {
155        return true;
156    }
157
158    protected MongoDBAuditBackend getMongoDBBackend() {
159        NXAuditEventsService auditEventsService = Framework.getService(NXAuditEventsService.class);
160        AuditBackend backend = auditEventsService.getBackend();
161        if (backend instanceof MongoDBAuditBackend) {
162            return (MongoDBAuditBackend) backend;
163        }
164        throw new NuxeoException(
165                "Unable to use MongoDBAuditPageProvider if auditEventsService service is not configured to run with MongoDB");
166    }
167
168    protected Bson buildAuditFilter() {
169        PageProviderDefinition def = getDefinition();
170        Object[] params = getParameters();
171        List<QuickFilter> quickFilters = getQuickFilters();
172        String quickFiltersClause = "";
173
174        if (quickFilters != null && !quickFilters.isEmpty()) {
175            for (QuickFilter quickFilter : quickFilters) {
176                String clause = quickFilter.getClause();
177                if (!quickFiltersClause.isEmpty() && clause != null) {
178                    quickFiltersClause = NXQLQueryBuilder.appendClause(quickFiltersClause, clause);
179                } else {
180                    quickFiltersClause = clause != null ? clause : "";
181                }
182            }
183        }
184
185        Bson filter;
186        WhereClauseDefinition whereClause = def.getWhereClause();
187        MongoDBAuditBackend mongoDBBackend = getMongoDBBackend();
188        if (whereClause == null) {
189            // Simple Pattern
190            if (!allowSimplePattern()) {
191                throw new UnsupportedOperationException("This page provider requires a explicit Where Clause");
192            }
193            String originalPattern = def.getPattern();
194            String pattern = quickFiltersClause.isEmpty() ? originalPattern
195                    : StringUtils.containsIgnoreCase(originalPattern, " WHERE ")
196                            ? NXQLQueryBuilder.appendClause(originalPattern, quickFiltersClause)
197                            : originalPattern + " WHERE " + quickFiltersClause;
198
199            String baseQuery = mongoDBBackend.expandQueryVariables(pattern, params);
200            filter = mongoDBBackend.buildFilter(baseQuery, null);
201        } else {
202
203            // Add the quick filters clauses to the fixed part
204            String fixedPart = getFixedPart();
205            if (StringUtils.isNotBlank(quickFiltersClause)) {
206                fixedPart = StringUtils.isNotBlank(fixedPart)
207                        ? NXQLQueryBuilder.appendClause(fixedPart, quickFiltersClause)
208                        : quickFiltersClause;
209            }
210
211            // Where clause based on DocumentModel
212            String baseQuery = mongoDBBackend.expandQueryVariables(fixedPart, params);
213            filter = buildSearchFilter(baseQuery, whereClause.getPredicates(), getSearchDocumentModel());
214        }
215        return filter;
216    }
217
218    @Override
219    public void refresh() {
220        setCurrentPageOffset(0);
221        super.refresh();
222    }
223
224    @Override
225    public long getResultsCount() {
226        return resultsCount;
227    }
228
229    @Override
230    public List<SortInfo> getSortInfos() {
231        // because ContentView can reuse PageProvider without redefining columns
232        // ensure compat for ContentView configured with JPA log.* sort syntax
233        List<SortInfo> sortInfos = super.getSortInfos();
234        for (SortInfo si : sortInfos) {
235            if (si.getSortColumn().startsWith("log.")) {
236                si.setSortColumn(si.getSortColumn().substring(4));
237            }
238        }
239        return sortInfos;
240    }
241
242    private Bson buildFilter(PredicateDefinition[] predicates, DocumentModel searchDocumentModel) {
243        if (searchDocumentModel == null) {
244            return new Document();
245        }
246
247        List<Bson> filters = new ArrayList<>();
248
249        for (PredicateDefinition predicate : predicates) {
250
251            // extract data from DocumentModel
252            PredicateFieldDefinition[] fieldDef = predicate.getValues();
253            Object[] val = new Object[fieldDef.length];
254            for (int fidx = 0; fidx < fieldDef.length; fidx++) {
255                Object value;
256                if (fieldDef[fidx].getXpath() != null) {
257                    value = searchDocumentModel.getPropertyValue(fieldDef[fidx].getXpath());
258                } else {
259                    value = searchDocumentModel.getProperty(fieldDef[fidx].getSchema(), fieldDef[fidx].getName());
260                }
261                // Convert Calendar objects
262                if (value instanceof Calendar) {
263                    value = ((Calendar) value).getTime();
264                }
265                val[fidx] = value;
266            }
267
268            if (!isNonNullParam(val)) {
269                // skip predicate where all values are null
270                continue;
271            }
272
273            String op = predicate.getOperator();
274            Object firstValue = val[0];
275            String predicateParameter = predicate.getParameter();
276            if (op.equalsIgnoreCase("IN")) {
277
278                String[] values;
279                if (firstValue instanceof Iterable<?>) {
280                    List<String> l = new ArrayList<>();
281                    Iterable<?> vals = (Iterable<?>) firstValue;
282
283                    for (Object v : vals) {
284                        if (v != null) {
285                            l.add(v.toString());
286                        }
287                    }
288                    values = l.toArray(new String[l.size()]);
289                } else if (firstValue instanceof Object[]) {
290                    values = (String[]) firstValue;
291                } else {
292                    throw new NuxeoException("IN operand required a list or an array as parameter");
293                }
294                filters.add(Filters.in(predicateParameter, values));
295            } else if (op.equalsIgnoreCase("BETWEEN")) {
296                filters.add(Filters.gt(predicateParameter, firstValue));
297                if (val.length > 1) {
298                    filters.add(Filters.lt(predicateParameter, val[1]));
299                }
300            } else if (">".equals(op)) {
301                filters.add(Filters.gt(predicateParameter, firstValue));
302            } else if (">=".equals(op)) {
303                filters.add(Filters.gte(predicateParameter, firstValue));
304            } else if ("<".equals(op)) {
305                filters.add(Filters.lt(predicateParameter, firstValue));
306            } else if ("<=".equals(op)) {
307                filters.add(Filters.lte(predicateParameter, firstValue));
308            } else {
309                filters.add(Filters.eq(predicateParameter, firstValue));
310            }
311        }
312
313        if (filters.isEmpty()) {
314            return new Document();
315        } else {
316            return Filters.and(filters);
317        }
318    }
319
320    private Bson buildSearchFilter(String fixedPart, PredicateDefinition[] predicates,
321            DocumentModel searchDocumentModel) {
322        try {
323            Document fixedFilter = Document.parse(fixedPart);
324            BsonDocument filter = buildFilter(predicates, searchDocumentModel).toBsonDocument(BsonDocument.class,
325                    getMongoDBBackend().getAuditCollection().getCodecRegistry());
326            if (fixedFilter.isEmpty()) {
327                return filter;
328            } else if (filter.isEmpty()) {
329                return fixedFilter;
330            } else {
331                return Filters.and(fixedFilter, filter);
332            }
333        } catch (RuntimeException e) { // NOSONAR
334            // catch everything to detect what's going wrong, see NXP-29101
335            log.error("Error during search filter construction, fixedPart: {}, predicates: {}, searchDocumentModel: {}",
336                    fixedPart, predicates, searchDocumentModel);
337            throw new NuxeoException("Unable to build search filter", e);
338        }
339    }
340
341    private boolean isNonNullParam(Object[] val) {
342        if (val == null) {
343            return false;
344        }
345        for (Object v : val) {
346            if (v instanceof String) {
347                if (!((String) v).isEmpty()) {
348                    return true;
349                }
350            } else if (v instanceof String[]) {
351                if (((String[]) v).length > 0) {
352                    return true;
353                }
354            } else if (v != null) {
355                return true;
356            }
357        }
358        return false;
359    }
360
361}