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