001/*
002 * (C) Copyright 2006-2017 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 *     Stephane Lacoin (Nuxeo EP Software Engineer)
018 */
019package org.nuxeo.ecm.platform.audit.service;
020
021import java.util.Date;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Map.Entry;
026import java.util.Set;
027import java.util.function.Function;
028
029import javax.persistence.EntityManager;
030import javax.persistence.Query;
031
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.nuxeo.ecm.core.query.sql.model.Literals;
035import org.nuxeo.ecm.core.query.sql.model.MultiExpression;
036import org.nuxeo.ecm.core.query.sql.model.Operand;
037import org.nuxeo.ecm.core.query.sql.model.Operator;
038import org.nuxeo.ecm.core.query.sql.model.OrderByExpr;
039import org.nuxeo.ecm.core.query.sql.model.OrderByList;
040import org.nuxeo.ecm.core.query.sql.model.Predicate;
041import org.nuxeo.ecm.core.query.sql.model.QueryBuilder;
042import org.nuxeo.ecm.core.query.sql.model.Reference;
043import org.nuxeo.ecm.platform.audit.api.FilterMapEntry;
044import org.nuxeo.ecm.platform.audit.api.LogEntry;
045import org.nuxeo.ecm.platform.audit.api.query.AuditQueryException;
046import org.nuxeo.ecm.platform.audit.api.query.DateRangeParser;
047import org.nuxeo.ecm.platform.audit.impl.LogEntryImpl;
048
049public class LogEntryProvider implements BaseLogEntryProvider {
050
051    private static final Log log = LogFactory.getLog(LogEntryProvider.class);
052
053    public static final String LIKE = "LIKE";
054
055    protected final EntityManager em;
056
057    private LogEntryProvider(EntityManager em) {
058        this.em = em;
059    }
060
061    public static LogEntryProvider createProvider(EntityManager em) {
062        return new LogEntryProvider(em);
063    }
064
065    public void append(List<LogEntry> entries) {
066        entries.forEach(e -> {
067            if (em.contains(e)) {
068                log.warn("Log entry already exists for id " + e.getId());
069            }
070            em.merge(e);
071        });
072    }
073
074    protected void doPersist(LogEntry entry) {
075        // Set the log date in java right before saving to the database. We
076        // cannot set a static column definition to
077        // "TIMESTAMP DEFAULT CURRENT_TIMESTAMP" as MS SQL Server does not
078        // support the TIMESTAMP column type and generating a dynamic
079        // persistence configuration that would depend on the database is too
080        // complicated.
081        entry.setLogDate(new Date());
082        em.persist(entry);
083    }
084
085    protected List<?> doPublishIfEntries(List<?> entries) {
086        if (entries == null || entries.size() == 0) {
087            return entries;
088        }
089        Object entry = entries.get(0);
090        if (entry instanceof LogEntry) {
091            for (Object logEntry : entries) {
092                doPublish((LogEntry) logEntry);
093            }
094        }
095        return entries;
096    }
097
098    protected List<LogEntry> doPublish(List<LogEntry> entries) {
099        entries.forEach(this::doPublish);
100        return entries;
101    }
102
103    protected LogEntry doPublish(LogEntry entry) {
104        if (entry.getExtendedInfos() != null) {
105            entry.getExtendedInfos().size(); // force lazy loading
106        }
107        return entry;
108    }
109
110    /*
111     * (non-Javadoc)
112     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#addLogEntry(org
113     * .nuxeo.ecm.platform.audit.api.LogEntry)
114     */
115    @Override
116    public void addLogEntry(LogEntry entry) {
117        doPersist(entry);
118    }
119
120    public void addLogEntries(List<LogEntry> entries) {
121        entries.forEach(this::doPersist);
122    }
123
124    @SuppressWarnings("unchecked")
125    @Override
126    public List<LogEntry> getLogEntriesFor(String uuid, String repositoryId) {
127        if (log.isDebugEnabled()) {
128            log.debug("getLogEntriesFor() UUID=" + uuid + " and repositoryId=" + repositoryId);
129        }
130        Query query = em.createNamedQuery("LogEntry.findByDocumentAndRepository");
131        query.setParameter("docUUID", uuid);
132        query.setParameter("repositoryId", repositoryId);
133        return doPublish(query.getResultList());
134    }
135
136    @SuppressWarnings("unchecked")
137    @Override
138    public List<LogEntry> getLogEntriesFor(String uuid) {
139        if (log.isDebugEnabled()) {
140            log.debug("getLogEntriesFor() UUID=" + uuid);
141        }
142        Query query = em.createNamedQuery("LogEntry.findByDocument");
143        query.setParameter("docUUID", uuid);
144        return doPublish(query.getResultList());
145    }
146
147    @SuppressWarnings("unchecked")
148    @Override
149    public List<LogEntry> getLogEntriesFor(String uuid, Map<String, FilterMapEntry> filterMap, boolean doDefaultSort) {
150        if (log.isDebugEnabled()) {
151            log.debug("getLogEntriesFor() UUID=" + uuid);
152        }
153
154        if (filterMap == null) {
155            filterMap = new HashMap<>();
156        }
157
158        StringBuilder queryStr = new StringBuilder();
159        queryStr.append(" FROM LogEntry log WHERE log.docUUID=:uuid ");
160
161        Set<String> filterMapKeySet = filterMap.keySet();
162        for (String currentKey : filterMapKeySet) {
163            FilterMapEntry currentFilterMapEntry = filterMap.get(currentKey);
164            String currentOperator = currentFilterMapEntry.getOperator();
165            String currentQueryParameterName = currentFilterMapEntry.getQueryParameterName();
166            String currentColumnName = currentFilterMapEntry.getColumnName();
167
168            if (LIKE.equals(currentOperator)) {
169                queryStr.append(" AND log.")
170                        .append(currentColumnName)
171                        .append(" LIKE :")
172                        .append(currentQueryParameterName)
173                        .append(" ");
174            } else {
175                queryStr.append(" AND log.")
176                        .append(currentColumnName)
177                        .append(currentOperator)
178                        .append(":")
179                        .append(currentQueryParameterName)
180                        .append(" ");
181            }
182        }
183
184        if (doDefaultSort) {
185            queryStr.append(" ORDER BY log.eventDate DESC");
186        }
187
188        Query query = em.createQuery(queryStr.toString());
189
190        query.setParameter("uuid", uuid);
191
192        for (String currentKey : filterMapKeySet) {
193            FilterMapEntry currentFilterMapEntry = filterMap.get(currentKey);
194            String currentOperator = currentFilterMapEntry.getOperator();
195            String currentQueryParameterName = currentFilterMapEntry.getQueryParameterName();
196            Object currentObject = currentFilterMapEntry.getObject();
197
198            if (LIKE.equals(currentOperator)) {
199                query.setParameter(currentQueryParameterName, "%" + currentObject + "%");
200            } else {
201                query.setParameter(currentQueryParameterName, currentObject);
202            }
203        }
204
205        return doPublish(query.getResultList());
206    }
207
208    /*
209     * (non-Javadoc)
210     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#getLogEntryByID (long)
211     */
212    public LogEntry getLogEntryByID(long id) {
213        if (log.isDebugEnabled()) {
214            log.debug("getLogEntriesFor() logID=" + id);
215        }
216        return doPublish(em.find(LogEntryImpl.class, id));
217    }
218
219    /*
220     * (non-Javadoc)
221     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#nativeQueryLogs (java.lang.String, int, int)
222     */
223    @SuppressWarnings("unchecked")
224    public List<LogEntry> nativeQueryLogs(String whereClause, int pageNb, int pageSize) {
225        Query query = em.createQuery("from LogEntry log where " + whereClause);
226        if (pageNb > 1) {
227            query.setFirstResult((pageNb - 1) * pageSize);
228        } else if (pageNb == 0) {
229            log.warn("Requested pageNb equals 0 but page index start at 1. Will fallback to fetch the first page");
230        }
231        query.setMaxResults(pageSize);
232        return doPublish(query.getResultList());
233    }
234
235    /*
236     * (non-Javadoc)
237     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#nativeQuery(java .lang.String, int, int)
238     */
239    public List<?> nativeQuery(String queryString, int pageNb, int pageSize) {
240        Query query = em.createQuery(queryString);
241        if (pageNb > 1) {
242            query.setFirstResult((pageNb - 1) * pageSize);
243        }
244        query.setMaxResults(pageSize);
245        return doPublishIfEntries(query.getResultList());
246    }
247
248    /*
249     * (non-Javadoc)
250     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#nativeQuery(java .lang.String, java.util.Map, int,
251     * int)
252     */
253    public List<?> nativeQuery(String queryString, Map<String, Object> params, int pageNb, int pageSize) {
254        if (pageSize <= 0) {
255            pageSize = 1000;
256        }
257        Query query = em.createQuery(queryString);
258        for (Entry<String, Object> en : params.entrySet()) {
259            query.setParameter(en.getKey(), en.getValue());
260        }
261        if (pageNb > 1) {
262            query.setFirstResult((pageNb - 1) * pageSize);
263        }
264        query.setMaxResults(pageSize);
265        return doPublishIfEntries(query.getResultList());
266    }
267
268    @SuppressWarnings("unchecked")
269    public List<LogEntry> queryLogs(QueryBuilder builder) {
270        if (log.isDebugEnabled()) {
271            log.debug("queryLogs() builder=" + builder);
272        }
273        // prepare parameters
274        MultiExpression multiExpression = builder.predicate();
275        OrderByList orders = builder.orders();
276        long offset = builder.offset();
277        long limit = builder.limit();
278        List<Predicate> predicates = multiExpression.predicates;
279        // current implementation only use Predicate/OrderByExpr with a simple Reference for left and right
280        Function<Operand, String> getFieldName = operand -> ((Reference) operand).name;
281
282        StringBuilder queryStr = new StringBuilder(" FROM LogEntry log");
283
284        // add predicate clauses
285        boolean firstFilter = true;
286        String op = multiExpression.operator == Operator.AND ? " AND" : " OR";
287        for (Predicate predicate : predicates) {
288            if (firstFilter) {
289                queryStr.append(" WHERE");
290                firstFilter = false;
291            } else {
292                queryStr.append(op);
293            }
294            String leftName = getFieldName.apply(predicate.lvalue);
295            queryStr.append(" log.")
296                    .append(leftName)
297                    .append(" ")
298                    .append(toString(predicate.operator))
299                    .append(" :")
300                    .append(leftName);
301        }
302
303        // add order clauses
304        boolean firstOrder = true;
305        for (OrderByExpr order : orders) {
306            if (firstOrder) {
307                queryStr.append(" ORDER BY");
308                firstOrder = false;
309            } else {
310                queryStr.append(",");
311            }
312            queryStr.append(" log.").append(getFieldName.apply(order.reference));
313        }
314        // if firstOrder == false then there's at least one order
315        if (!firstOrder) {
316            if (orders.get(0).isDescending) {
317                queryStr.append(" DESC");
318            } else {
319                queryStr.append(" ASC");
320            }
321        }
322
323        Query query = em.createQuery(queryStr.toString());
324
325        for (Predicate predicate : predicates) {
326            String leftName = getFieldName.apply(predicate.lvalue);
327            Operator operator = predicate.operator;
328            Object rightValue = Literals.valueOf(predicate.rvalue);
329            if (Operator.LIKE.equals(operator)) {
330                rightValue = "%" + rightValue + "%";
331            } else if (Operator.STARTSWITH.equals(operator)) {
332                rightValue = rightValue + "%";
333            }
334            query.setParameter(leftName, rightValue);
335        }
336
337        // add offset clause
338        if (offset > 0) {
339            query.setFirstResult((int) offset);
340        }
341
342        // add limit clause
343        if (limit > 0) {
344            query.setMaxResults((int) limit);
345        }
346
347        return doPublish(query.getResultList());
348    }
349
350    /**
351     * A string representation of an Operator
352     */
353    protected String toString(Operator operator) {
354        if (Operator.STARTSWITH.equals(operator)) {
355            return LIKE;
356        }
357        return operator.toString();
358    }
359
360    /*
361     * (non-Javadoc)
362     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#queryLogs(java. lang.String[], java.lang.String)
363     */
364    @SuppressWarnings("unchecked")
365    public List<LogEntry> queryLogs(String[] eventIds, String dateRange) {
366        Date limit;
367        try {
368            limit = DateRangeParser.parseDateRangeQuery(new Date(), dateRange);
369        } catch (AuditQueryException aqe) {
370            aqe.addInfo("Wrong date range query. Query was " + dateRange);
371            throw aqe;
372        }
373
374        String queryStr;
375        if (eventIds == null || eventIds.length == 0) {
376            queryStr = "from LogEntry log" + " where log.eventDate >= :limit" + " ORDER BY log.eventDate DESC";
377        } else {
378            String inClause = "(";
379            for (String eventId : eventIds) {
380                inClause += "'" + eventId + "',";
381            }
382            inClause = inClause.substring(0, inClause.length() - 1);
383            inClause += ")";
384
385            queryStr = "from LogEntry log" + " where log.eventId in " + inClause + " AND log.eventDate >= :limit"
386                    + " ORDER BY log.eventDate DESC";
387        }
388
389        if (log.isDebugEnabled()) {
390            log.debug("queryLogs() =" + queryStr);
391        }
392        Query query = em.createQuery(queryStr);
393        query.setParameter("limit", limit);
394
395        return doPublish(query.getResultList());
396    }
397
398    /*
399     * (non-Javadoc)
400     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#queryLogsByPage (java.lang.String[], java.lang.String,
401     * java.lang.String[], java.lang.String, int, int)
402     */
403    public List<LogEntry> queryLogsByPage(String[] eventIds, String dateRange, String[] categories, String path,
404            int pageNb, int pageSize) {
405        Date limit;
406        try {
407            limit = DateRangeParser.parseDateRangeQuery(new Date(), dateRange);
408        } catch (AuditQueryException aqe) {
409            aqe.addInfo("Wrong date range query. Query was " + dateRange);
410            throw aqe;
411        }
412        return queryLogsByPage(eventIds, limit, categories, path, pageNb, pageSize);
413    }
414
415    /*
416     * (non-Javadoc)
417     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#queryLogsByPage (java.lang.String[], java.util.Date,
418     * java.lang.String[], java.lang.String, int, int)
419     */
420    @SuppressWarnings("unchecked")
421    public List<LogEntry> queryLogsByPage(String[] eventIds, Date limit, String[] categories, String path, int pageNb,
422            int pageSize) {
423        if (eventIds == null) {
424            eventIds = new String[0];
425        }
426        if (categories == null) {
427            categories = new String[0];
428        }
429
430        StringBuilder queryString = new StringBuilder();
431
432        queryString.append("from LogEntry log where ");
433
434        if (eventIds.length > 0) {
435            String inClause = "(";
436            for (String eventId : eventIds) {
437                inClause += "'" + eventId + "',";
438            }
439            inClause = inClause.substring(0, inClause.length() - 1);
440            inClause += ")";
441
442            queryString.append(" log.eventId IN ").append(inClause);
443            queryString.append(" AND ");
444        }
445        if (categories.length > 0) {
446            String inClause = "(";
447            for (String cat : categories) {
448                inClause += "'" + cat + "',";
449            }
450            inClause = inClause.substring(0, inClause.length() - 1);
451            inClause += ")";
452            queryString.append(" log.category IN ").append(inClause);
453            queryString.append(" AND ");
454        }
455
456        if (path != null && !"".equals(path.trim())) {
457            queryString.append(" log.docPath LIKE '").append(path).append("%'");
458            queryString.append(" AND ");
459        }
460
461        queryString.append(" log.eventDate >= :limit");
462        queryString.append(" ORDER BY log.eventDate DESC");
463
464        Query query = em.createQuery(queryString.toString());
465
466        query.setParameter("limit", limit);
467
468        if (pageNb > 1) {
469            query.setFirstResult((pageNb - 1) * pageSize);
470        }
471        query.setMaxResults(pageSize);
472
473        return doPublish(query.getResultList());
474    }
475
476    /*
477     * (non-Javadoc)
478     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#removeEntries(java .lang.String, java.lang.String)
479     */
480    @Override
481    @SuppressWarnings("unchecked")
482    public int removeEntries(String eventId, String pathPattern) {
483        // TODO extended info cascade delete does not work using HQL, so we
484        // have to delete each
485        // entry by hand.
486        Query query = em.createNamedQuery("LogEntry.findByEventIdAndPath");
487        query.setParameter("eventId", eventId);
488        query.setParameter("pathPattern", pathPattern + "%");
489        int count = 0;
490        for (LogEntry entry : (List<LogEntry>) query.getResultList()) {
491            em.remove(entry);
492            count += 1;
493        }
494        if (log.isDebugEnabled()) {
495            log.debug("removed " + count + " entries from " + pathPattern);
496        }
497        return count;
498    }
499
500    /*
501     * (non-Javadoc)
502     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#countEventsById (java.lang.String)
503     */
504    public Long countEventsById(String eventId) {
505        Query query = em.createNamedQuery("LogEntry.countEventsById");
506        query.setParameter("eventId", eventId);
507        return (Long) query.getSingleResult();
508    }
509
510    /*
511     * (non-Javadoc)
512     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#findEventIds()
513     */
514    @SuppressWarnings("unchecked")
515    public List<String> findEventIds() {
516        Query query = em.createNamedQuery("LogEntry.findEventIds");
517        return (List<String>) query.getResultList();
518    }
519}