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