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}