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.runtime.api.Framework;
049import org.nuxeo.runtime.mongodb.MongoDBSerializationHelper;
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            @SuppressWarnings("resource")
087            CoreSession session = getCoreSession();
088            if (session != null) {
089                CommentProcessorHelper cph = new CommentProcessorHelper(session);
090                cph.processComments(entries);
091            }
092        }
093    }
094
095    @Override
096    public List<LogEntry> getCurrentPage() {
097        long t0 = System.currentTimeMillis();
098
099        Bson filter = buildAuditFilter();
100
101        List<SortInfo> sortInfos = getSortInfos();
102        List<Bson> sorts = new ArrayList<>(sortInfos.size());
103        for (SortInfo sortInfo : sortInfos) {
104            String sortColumn = sortInfo.getSortColumn();
105            if ("id".equals(sortColumn)) {
106                sortColumn = MongoDBSerializationHelper.MONGODB_ID;
107            }
108            if (sortInfo.getSortAscending()) {
109                sorts.add(Sorts.ascending(sortColumn));
110            } else {
111                sorts.add(Sorts.descending(sortColumn));
112            }
113        }
114        MongoCollection<Document> auditCollection = getMongoDBBackend().getAuditCollection();
115        FindIterable<Document> response = auditCollection.find(filter)
116                                                         .sort(Sorts.orderBy(sorts))
117                                                         .skip((int) (getCurrentPageIndex() * pageSize))
118                                                         .limit((int) getMinMaxPageSize());
119        List<LogEntry> entries = new ArrayList<>();
120
121        // set total number of results
122        setResultsCount(auditCollection.count(filter));
123
124        for (Document document : response) {
125            entries.add(MongoDBAuditEntryReader.read(document));
126        }
127        preprocessCommentsIfNeeded(entries);
128
129        CoreSession session = getCoreSession();
130        if (session != null) {
131            // send event for statistics !
132            fireSearchEvent(session.getPrincipal(), filter.toString(), entries, System.currentTimeMillis() - t0);
133        }
134
135        return entries;
136    }
137
138    protected String getFixedPart() {
139        WhereClauseDefinition whereClause = getDefinition().getWhereClause();
140        if (whereClause == null) {
141            return null;
142        } else {
143            String fixedPart = whereClause.getFixedPart();
144            if (fixedPart == null || fixedPart.isEmpty()) {
145                fixedPart = EMPTY_QUERY;
146            }
147            return fixedPart;
148        }
149    }
150
151    protected boolean allowSimplePattern() {
152        return true;
153    }
154
155    protected MongoDBAuditBackend getMongoDBBackend() {
156        NXAuditEventsService audit = (NXAuditEventsService) Framework.getRuntime()
157                                                                     .getComponent(NXAuditEventsService.NAME);
158        AuditBackend backend = audit.getBackend();
159        if (backend instanceof MongoDBAuditBackend) {
160            return (MongoDBAuditBackend) backend;
161        }
162        throw new NuxeoException(
163                "Unable to use MongoDBAuditPageProvider if audit service is not configured to run with MongoDB");
164    }
165
166    protected Bson buildAuditFilter() {
167        PageProviderDefinition def = getDefinition();
168        Object[] params = getParameters();
169        List<QuickFilter> quickFilters = getQuickFilters();
170        String quickFiltersClause = "";
171
172        if (quickFilters != null && !quickFilters.isEmpty()) {
173            for (QuickFilter quickFilter : quickFilters) {
174                String clause = quickFilter.getClause();
175                if (!quickFiltersClause.isEmpty() && clause != null) {
176                    quickFiltersClause = NXQLQueryBuilder.appendClause(quickFiltersClause, clause);
177                } else {
178                    quickFiltersClause = clause != null ? clause : "";
179                }
180            }
181        }
182
183        Bson filter;
184        WhereClauseDefinition whereClause = def.getWhereClause();
185        MongoDBAuditBackend mongoDBBackend = getMongoDBBackend();
186        if (whereClause == null) {
187            // Simple Pattern
188            if (!allowSimplePattern()) {
189                throw new UnsupportedOperationException("This page provider requires a explicit Where Clause");
190            }
191            String originalPattern = def.getPattern();
192            String pattern = quickFiltersClause.isEmpty() ? originalPattern
193                    : StringUtils.containsIgnoreCase(originalPattern, " WHERE ")
194                            ? NXQLQueryBuilder.appendClause(originalPattern, quickFiltersClause)
195                            : originalPattern + " WHERE " + quickFiltersClause;
196
197            String baseQuery = mongoDBBackend.expandQueryVariables(pattern, params);
198            filter = mongoDBBackend.buildFilter(baseQuery, null);
199        } else {
200
201            // Add the quick filters clauses to the fixed part
202            String fixedPart = getFixedPart();
203            if (StringUtils.isNotBlank(quickFiltersClause)) {
204                fixedPart = StringUtils.isNotBlank(fixedPart)
205                        ? NXQLQueryBuilder.appendClause(fixedPart, quickFiltersClause) : quickFiltersClause;
206            }
207
208            // Where clause based on DocumentModel
209            String baseQuery = mongoDBBackend.expandQueryVariables(fixedPart, params);
210            filter = buildSearchFilter(baseQuery, whereClause.getPredicates(), getSearchDocumentModel());
211        }
212        return filter;
213    }
214
215    @Override
216    public void refresh() {
217        setCurrentPageOffset(0);
218        super.refresh();
219    }
220
221    @Override
222    public long getResultsCount() {
223        return resultsCount;
224    }
225
226    @Override
227    public List<SortInfo> getSortInfos() {
228        // because ContentView can reuse PageProvider without redefining columns
229        // ensure compat for ContentView configured with JPA log.* sort syntax
230        List<SortInfo> sortInfos = super.getSortInfos();
231        for (SortInfo si : sortInfos) {
232            if (si.getSortColumn().startsWith("log.")) {
233                si.setSortColumn(si.getSortColumn().substring(4));
234            }
235        }
236        return sortInfos;
237    }
238
239    private Bson buildFilter(PredicateDefinition[] predicates, DocumentModel searchDocumentModel) {
240        if (searchDocumentModel == null) {
241            return new Document();
242        }
243
244        List<Bson> filters = new ArrayList<>();
245
246        for (PredicateDefinition predicate : predicates) {
247
248            // extract data from DocumentModel
249            PredicateFieldDefinition[] fieldDef = predicate.getValues();
250            Object[] val = new Object[fieldDef.length];
251            for (int fidx = 0; fidx < fieldDef.length; fidx++) {
252                Object value;
253                if (fieldDef[fidx].getXpath() != null) {
254                    value = searchDocumentModel.getPropertyValue(fieldDef[fidx].getXpath());
255                } else {
256                    value = searchDocumentModel.getProperty(fieldDef[fidx].getSchema(), fieldDef[fidx].getName());
257                }
258                // Convert Calendar objects
259                if (value instanceof Calendar) {
260                    value = ((Calendar) value).getTime();
261                }
262                val[fidx] = value;
263            }
264
265            if (!isNonNullParam(val)) {
266                // skip predicate where all values are null
267                continue;
268            }
269
270            String op = predicate.getOperator();
271            Object firstValue = val[0];
272            String predicateParameter = predicate.getParameter();
273            if (op.equalsIgnoreCase("IN")) {
274
275                String[] values;
276                if (firstValue instanceof Iterable<?>) {
277                    List<String> l = new ArrayList<>();
278                    Iterable<?> vals = (Iterable<?>) firstValue;
279
280                    for (Object v : vals) {
281                        if (v != null) {
282                            l.add(v.toString());
283                        }
284                    }
285                    values = l.toArray(new String[l.size()]);
286                } else if (firstValue instanceof Object[]) {
287                    values = (String[]) firstValue;
288                } else {
289                    throw new NuxeoException("IN operand required a list or an array as parameter");
290                }
291                filters.add(Filters.in(predicateParameter, values));
292            } else if (op.equalsIgnoreCase("BETWEEN")) {
293                filters.add(Filters.gt(predicateParameter, firstValue));
294                if (val.length > 1) {
295                    filters.add(Filters.lt(predicateParameter, val[1]));
296                }
297            } else if (">".equals(op)) {
298                filters.add(Filters.gt(predicateParameter, firstValue));
299            } else if (">=".equals(op)) {
300                filters.add(Filters.gte(predicateParameter, firstValue));
301            } else if ("<".equals(op)) {
302                filters.add(Filters.lt(predicateParameter, firstValue));
303            } else if ("<=".equals(op)) {
304                filters.add(Filters.lte(predicateParameter, firstValue));
305            } else {
306                filters.add(Filters.eq(predicateParameter, firstValue));
307            }
308        }
309
310        if (filters.isEmpty()) {
311            return new Document();
312        } else {
313            return Filters.and(filters);
314        }
315    }
316
317    private Bson buildSearchFilter(String fixedPart, PredicateDefinition[] predicates,
318            DocumentModel searchDocumentModel) {
319        Document fixedFilter = Document.parse(fixedPart);
320        BsonDocument filter = buildFilter(predicates, searchDocumentModel).toBsonDocument(BsonDocument.class,
321                getMongoDBBackend().getAuditCollection().getCodecRegistry());
322        if (fixedFilter.isEmpty()) {
323            return filter;
324        } else if (filter.isEmpty()) {
325            return fixedFilter;
326        } else {
327            return Filters.and(fixedFilter, filter);
328        }
329    }
330
331    private boolean isNonNullParam(Object[] val) {
332        if (val == null) {
333            return false;
334        }
335        for (Object v : val) {
336            if (v instanceof String) {
337                if (!((String) v).isEmpty()) {
338                    return true;
339                }
340            } else if (v instanceof String[]) {
341                if (((String[]) v).length > 0) {
342                    return true;
343                }
344            } else if (v != null) {
345                return true;
346            }
347        }
348        return false;
349    }
350
351}