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}