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