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