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}