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    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(AuditQueryBuilder builder) {
270        if (log.isDebugEnabled()) {
271            log.debug("queryLogs() builder=" + builder);
272        }
273        // prepare parameters
274        Predicate andPredicate = builder.predicate();
275        OrderByList orders = builder.orders();
276        long offset = builder.offset();
277        long limit = builder.limit();
278        // cast parameters
279        // current implementation only support a MultiExpression with AND operator
280        List<Predicate> predicates = (List<Predicate>) ((List<?>) ((MultiExpression) andPredicate).values);
281        // current implementation only use Predicate/OrderByExpr with a simple Reference for left and right
282        Function<Operand, String> getFieldName = operand -> ((Reference) operand).name;
283
284        StringBuilder queryStr = new StringBuilder(" FROM LogEntry log");
285
286        // add predicate clauses
287        boolean firstFilter = true;
288        for (Predicate predicate : predicates) {
289            if (firstFilter) {
290                queryStr.append(" WHERE");
291                firstFilter = false;
292            } else {
293                queryStr.append(" AND");
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                    .append(leftName);
302        }
303
304        // add order clauses
305        boolean firstOrder = true;
306        for (OrderByExpr order : orders) {
307            if (firstOrder) {
308                queryStr.append(" ORDER BY");
309                firstOrder = false;
310            } else {
311                queryStr.append(",");
312            }
313            queryStr.append(" log.").append(getFieldName.apply(order.reference));
314        }
315        // if firstOrder == false then there's at least one order
316        if (!firstOrder) {
317            if (orders.get(0).isDescending) {
318                queryStr.append(" DESC");
319            } else {
320                queryStr.append(" ASC");
321            }
322        }
323
324        Query query = em.createQuery(queryStr.toString());
325
326        for (Predicate predicate : predicates) {
327            String leftName = getFieldName.apply(predicate.lvalue);
328            Operator operator = predicate.operator;
329            Object rightValue = Literals.valueOf(predicate.rvalue);
330            if (Operator.LIKE.equals(operator)) {
331                rightValue = "%" + rightValue + "%";
332            } else if (Operator.STARTSWITH.equals(operator)) {
333                rightValue = rightValue + "%";
334            }
335            query.setParameter(leftName, rightValue);
336        }
337
338        // add offset clause
339        if (offset > 0) {
340            query.setFirstResult((int) offset);
341        }
342
343        // add limit clause
344        if (limit > 0) {
345            query.setMaxResults((int) limit);
346        }
347
348        return doPublish(query.getResultList());
349    }
350
351    /**
352     * A string representation of an Operator
353     */
354    protected String toString(Operator operator) {
355        if (Operator.STARTSWITH.equals(operator)) {
356            return LIKE;
357        }
358        return operator.toString();
359    }
360
361    /*
362     * (non-Javadoc)
363     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#queryLogs(java. lang.String[], java.lang.String)
364     */
365    @SuppressWarnings("unchecked")
366    public List<LogEntry> queryLogs(String[] eventIds, String dateRange) {
367        Date limit;
368        try {
369            limit = DateRangeParser.parseDateRangeQuery(new Date(), dateRange);
370        } catch (AuditQueryException aqe) {
371            aqe.addInfo("Wrong date range query. Query was " + dateRange);
372            throw aqe;
373        }
374
375        String queryStr;
376        if (eventIds == null || eventIds.length == 0) {
377            queryStr = "from LogEntry log" + " where log.eventDate >= :limit" + " ORDER BY log.eventDate DESC";
378        } else {
379            String inClause = "(";
380            for (String eventId : eventIds) {
381                inClause += "'" + eventId + "',";
382            }
383            inClause = inClause.substring(0, inClause.length() - 1);
384            inClause += ")";
385
386            queryStr = "from LogEntry log" + " where log.eventId in " + inClause + " AND log.eventDate >= :limit"
387                    + " ORDER BY log.eventDate DESC";
388        }
389
390        if (log.isDebugEnabled()) {
391            log.debug("queryLogs() =" + queryStr);
392        }
393        Query query = em.createQuery(queryStr);
394        query.setParameter("limit", limit);
395
396        return doPublish(query.getResultList());
397    }
398
399    /*
400     * (non-Javadoc)
401     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#queryLogsByPage (java.lang.String[], java.lang.String,
402     * java.lang.String[], java.lang.String, int, int)
403     */
404    public List<LogEntry> queryLogsByPage(String[] eventIds, String dateRange, String[] categories, String path,
405            int pageNb, int pageSize) {
406        Date limit;
407        try {
408            limit = DateRangeParser.parseDateRangeQuery(new Date(), dateRange);
409        } catch (AuditQueryException aqe) {
410            aqe.addInfo("Wrong date range query. Query was " + dateRange);
411            throw aqe;
412        }
413        return queryLogsByPage(eventIds, limit, categories, path, pageNb, pageSize);
414    }
415
416    /*
417     * (non-Javadoc)
418     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#queryLogsByPage (java.lang.String[], java.util.Date,
419     * java.lang.String[], java.lang.String, int, int)
420     */
421    @SuppressWarnings("unchecked")
422    public List<LogEntry> queryLogsByPage(String[] eventIds, Date limit, String[] categories, String path, int pageNb,
423            int pageSize) {
424        if (eventIds == null) {
425            eventIds = new String[0];
426        }
427        if (categories == null) {
428            categories = new String[0];
429        }
430
431        StringBuilder queryString = new StringBuilder();
432
433        queryString.append("from LogEntry log where ");
434
435        if (eventIds.length > 0) {
436            String inClause = "(";
437            for (String eventId : eventIds) {
438                inClause += "'" + eventId + "',";
439            }
440            inClause = inClause.substring(0, inClause.length() - 1);
441            inClause += ")";
442
443            queryString.append(" log.eventId IN ").append(inClause);
444            queryString.append(" AND ");
445        }
446        if (categories.length > 0) {
447            String inClause = "(";
448            for (String cat : categories) {
449                inClause += "'" + cat + "',";
450            }
451            inClause = inClause.substring(0, inClause.length() - 1);
452            inClause += ")";
453            queryString.append(" log.category IN ").append(inClause);
454            queryString.append(" AND ");
455        }
456
457        if (path != null && !"".equals(path.trim())) {
458            queryString.append(" log.docPath LIKE '").append(path).append("%'");
459            queryString.append(" AND ");
460        }
461
462        queryString.append(" log.eventDate >= :limit");
463        queryString.append(" ORDER BY log.eventDate DESC");
464
465        Query query = em.createQuery(queryString.toString());
466
467        query.setParameter("limit", limit);
468
469        if (pageNb > 1) {
470            query.setFirstResult((pageNb - 1) * pageSize);
471        }
472        query.setMaxResults(pageSize);
473
474        return doPublish(query.getResultList());
475    }
476
477    /*
478     * (non-Javadoc)
479     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#removeEntries(java .lang.String, java.lang.String)
480     */
481    @Override
482    @SuppressWarnings("unchecked")
483    public int removeEntries(String eventId, String pathPattern) {
484        // TODO extended info cascade delete does not work using HQL, so we
485        // have to delete each
486        // entry by hand.
487        Query query = em.createNamedQuery("LogEntry.findByEventIdAndPath");
488        query.setParameter("eventId", eventId);
489        query.setParameter("pathPattern", pathPattern + "%");
490        int count = 0;
491        for (LogEntry entry : (List<LogEntry>) query.getResultList()) {
492            em.remove(entry);
493            count += 1;
494        }
495        if (log.isDebugEnabled()) {
496            log.debug("removed " + count + " entries from " + pathPattern);
497        }
498        return count;
499    }
500
501    /*
502     * (non-Javadoc)
503     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#countEventsById (java.lang.String)
504     */
505    public Long countEventsById(String eventId) {
506        Query query = em.createNamedQuery("LogEntry.countEventsById");
507        query.setParameter("eventId", eventId);
508        return (Long) query.getSingleResult();
509    }
510
511    /*
512     * (non-Javadoc)
513     * @see org.nuxeo.ecm.platform.audit.service.LogEntryProvider#findEventIds()
514     */
515    @SuppressWarnings("unchecked")
516    public List<String> findEventIds() {
517        Query query = em.createNamedQuery("LogEntry.findEventIds");
518        return (List<String>) query.getResultList();
519    }
520}