Commit 807de2c2 authored by Matija Obreza's avatar Matija Obreza
Browse files

Audits: New approach to recording changes

parent b973e590
......@@ -40,6 +40,7 @@ import org.genesys.blocks.model.EmptyModel;
import org.genesys.blocks.util.JsonSidConverter;
import org.hibernate.annotations.Type;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import com.fasterxml.jackson.annotation.JsonProperty;
......@@ -69,6 +70,7 @@ public class AuditLog extends EmptyModel {
/** The log date. */
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "logdate", nullable = false)
@CreatedDate
private Date logDate;
/** Class name of the referenced entity. */
......@@ -358,4 +360,14 @@ public class AuditLog extends EmptyModel {
public Object getNewEntity() {
return newEntity;
}
@Override
public String toString() {
StringBuffer sb=new StringBuffer();
sb.append(this.logDate).append(" ").append(this.action).append(" ");
sb.append(this.classPk.getClassname()).append("#").append(this.entityId);
sb.append(this.propertyName).append(" ");
sb.append(this.previousState).append(" -> ").append(this.newState);
return sb.toString();
}
}
......@@ -28,24 +28,6 @@ public class TransactionAuditLog {
/** ID of the referenced entity. */
private long entityId;
/** The name of the property modified. */
private String propertyName;
/** The type of entity referenced in the changed property. */
private ClassPK referencedEntity;
/** String representation of the previous state. */
private String previousState;
/** String representation of the updated state. */
private String newState;
/** The previous object */
private Object previousObject;
/** The new object */
private Object newObject;
/** The action. */
private AuditAction action;
......@@ -85,78 +67,6 @@ public class TransactionAuditLog {
this.entityId = entityId;
}
/**
* Sets the property name.
*
* @param propertyName the new property name
*/
public void setPropertyName(final String propertyName) {
this.propertyName = propertyName;
}
/**
* Gets the property name.
*
* @return the property name
*/
public String getPropertyName() {
return propertyName;
}
/**
* Sets the referenced entity.
*
* @param referencedEntity the new referenced entity
*/
public void setReferencedEntity(final ClassPK referencedEntity) {
this.referencedEntity = referencedEntity;
}
/**
* Gets the referenced entity.
*
* @return the referenced entity
*/
public ClassPK getReferencedEntity() {
return referencedEntity;
}
/**
* Sets the previous state.
*
* @param previousState the new previous state
*/
public void setPreviousState(final String previousState) {
this.previousState = previousState;
}
/**
* Gets the previous state.
*
* @return the previous state
*/
public String getPreviousState() {
return previousState;
}
/**
* Sets the new state.
*
* @param newState the new new state
*/
public void setNewState(final String newState) {
this.newState = newState;
}
/**
* Gets the new state.
*
* @return the new state
*/
public String getNewState() {
return newState;
}
/**
* Sets the action.
*
......@@ -175,34 +85,6 @@ public class TransactionAuditLog {
return action;
}
/**
* @return the previousObject
*/
public Object getPreviousObject() {
return previousObject;
}
/**
* @param previousObject the previousObject to set
*/
public void setPreviousObject(Object previousObject) {
this.previousObject = previousObject;
}
/**
* @return the newObject
*/
public Object getNewObject() {
return newObject;
}
/**
* @param newObject the newObject to set
*/
public void setNewObject(Object newObject) {
this.newObject = newObject;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
......@@ -213,7 +95,6 @@ public class TransactionAuditLog {
int result = 1;
result = prime * result + ((classPk == null) ? 0 : classPk.hashCode());
result = prime * result + (int) (entityId ^ (entityId >>> 32));
result = prime * result + ((propertyName == null) ? 0 : propertyName.hashCode());
return result;
}
......@@ -237,20 +118,7 @@ public class TransactionAuditLog {
return false;
if (entityId != other.entityId)
return false;
if (propertyName == null) {
if (other.propertyName != null)
return false;
} else if (!propertyName.equals(other.propertyName))
return false;
return true;
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(action.toString()).append(" shortname=").append(classPk.getShortName()).append(" id=").append(entityId).append(" ").append(propertyName).append("='").append(previousState).append(
"' -> '").append(newState).append("'");
return sb.toString();
}
}
......@@ -15,14 +15,13 @@
*/
package org.genesys.blocks.auditlog.service;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.genesys.blocks.auditlog.component.AuditTrailInterceptor;
import org.apache.commons.lang3.tuple.Pair;
import org.genesys.blocks.auditlog.model.AuditAction;
import org.genesys.blocks.auditlog.model.AuditLog;
import org.genesys.blocks.auditlog.model.TransactionAuditLog;
import org.genesys.blocks.auditlog.model.filters.AuditLogFilter;
import org.genesys.blocks.model.EntityId;
import org.springframework.data.domain.Page;
......@@ -44,30 +43,13 @@ public interface AuditTrailService {
*/
Page<AuditLog> listAuditLogs(AuditLogFilter filters, Pageable page);
/**
* Create a temporary audit log entry. This is used by the
* {@link AuditTrailInterceptor}.
*
* @param action the action
* @param entity the entity
* @param id the id
* @param propertyName the property name
* @param previousState the previous state
* @param currentState the current state
* @param referencedEntity the referenced entity
* @param previousObject the previous object (not persisted)
* @param currentObject the current object (not persisted)
* @return the transaction audit log
*/
TransactionAuditLog auditLogEntry(AuditAction action, Object entity, long id, String propertyName, String previousState, String currentState, Class<?> referencedEntity, Object previousObject, Object currentObject);
/**
* Record audit logs in the database.
*
* @param auditLogs the audit logs
* @param currentAuditLogs the audit logs
* @return the list
*/
List<AuditLog> addAuditLogs(Set<TransactionAuditLog> auditLogs);
List<AuditLog> addAuditLogs(Map<ChangeKey, Map<String, PropertyChange<?>>> currentAuditLogs);
/**
* List audit logs for the specified entity. Logs are sorted by log date
......@@ -91,4 +73,89 @@ public interface AuditTrailService {
*/
Map<String, List<AuditLog>> auditLogs(EntityId entity);
static class ChangeKey {
public Class<?> clazz;
public long id;
public AuditAction type;
private ChangeKey(AuditAction type, Class<? extends Object> clazz, long id) {
this.clazz = clazz;
this.id = id;
this.type = type;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((clazz == null) ? 0 : clazz.hashCode());
result = prime * result + (int) (id ^ (id >>> 32));
result = prime * result + ((type == null) ? 0 : type.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ChangeKey other = (ChangeKey) obj;
if (clazz == null) {
if (other.clazz != null) {
return false;
}
} else if (!clazz.equals(other.clazz)) {
return false;
}
if (id != other.id) {
return false;
}
if (type != other.type) {
return false;
}
return true;
}
public static ChangeKey of(AuditAction type, Class<? extends Object> clazz, long id) {
return new ChangeKey(type, clazz, id);
}
}
static class PropertyChange<T> {
public Class<? extends T> clazz;
public Pair<Object, Object> change;
private PropertyChange(Class<T> clazz, Collection<?> previous, Collection<?> remaining) {
this.clazz = clazz;
this.change = Pair.of(previous, remaining);
}
private PropertyChange(Class<T> clazz, Object previous, Object remaining) {
this.clazz = clazz;
this.change = Pair.of(previous, remaining);
}
public static <T> PropertyChange<T> of(Class<T> clazz, Collection<?> previous, Collection<?> remaining) {
return new PropertyChange<T>(clazz, previous, remaining);
}
public static <T> PropertyChange<T> of(Class<T> propertyType, Object prev, Object curr) {
return new PropertyChange<>(propertyType, prev, curr);
}
@Override
public String toString() {
StringBuffer sb=new StringBuffer();
sb.append("[").append(this.clazz).append("]: ");
sb.append(this.change);
return sb.toString();
}
}
}
......@@ -15,18 +15,35 @@
*/
package org.genesys.blocks.auditlog.service.impl;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.text.Format;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.lang3.time.FastDateFormat;
import org.genesys.blocks.auditlog.annotations.Audited;
import org.genesys.blocks.auditlog.annotations.HideAuditValue;
import org.genesys.blocks.auditlog.annotations.NotAudited;
import org.genesys.blocks.auditlog.model.AuditAction;
import org.genesys.blocks.auditlog.model.AuditLog;
import org.genesys.blocks.auditlog.model.TransactionAuditLog;
import org.genesys.blocks.auditlog.model.filters.AuditLogFilter;
import org.genesys.blocks.auditlog.persistence.AuditLogRepository;
import org.genesys.blocks.auditlog.service.AuditTrailService;
......@@ -35,13 +52,15 @@ import org.genesys.blocks.model.ClassPK;
import org.genesys.blocks.model.EntityId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
/**
* Implementation of the {@link AuditTrailService} with JPA.
......@@ -50,7 +69,7 @@ import org.springframework.transaction.annotation.Transactional;
*/
@Service
@Transactional(readOnly = true)
public class AuditTrailServiceImpl implements AuditTrailService {
public class AuditTrailServiceImpl implements AuditTrailService, InitializingBean {
/** The Constant LOG. */
private static final Logger LOG = LoggerFactory.getLogger(AuditTrailServiceImpl.class);
......@@ -63,67 +82,100 @@ public class AuditTrailServiceImpl implements AuditTrailService {
@Autowired
private AuditLogRepository auditLogRepository;
/*
* (non-Javadoc)
* @see
* org.genesys.blocks.auditlog.service.AuditTrailService#addAuditLogs(java.util.
* Set)
*/
@Override
@Transactional(isolation = Isolation.READ_UNCOMMITTED, propagation = Propagation.REQUIRED)
public List<AuditLog> addAuditLogs(final Set<TransactionAuditLog> auditLogs) {
return auditLogRepository.saveAll(auditLogs.stream().map(tlog -> toAuditLog(tlog)).collect(Collectors.toList()));
}
/** The date format. */
private final String dateFormat = "dd-MMM-yyyy";
/*
* (non-Javadoc)
* @see org.genesys.blocks.auditlog.service.AuditTrailService#auditLogEntry(org.
* genesys.blocks.auditlog.model.AuditAction, java.lang.Object, long,
* java.lang.String, java.lang.String, java.lang.String, java.lang.Class)
*/
@Override
public TransactionAuditLog auditLogEntry(final AuditAction action, final Object entity, final long id, final String propertyName, final String previousState,
final String currentState, final Class<?> referencedEntity, final Object previousObject, final Object currentObject) {
/** The time format. */
private final String timeFormat = "HH:mm:ss";
/** The date time format. */
private final String dateTimeFormat = "dd-MMM-yyyy HH:mm:ss";
/** The date formatter. */
private Format dateFormatter = FastDateFormat.getInstance(dateFormat);
/** The date time formatter. */
private Format dateTimeFormatter = FastDateFormat.getInstance(dateTimeFormat);
/** The time formatter. */
private Format timeFormatter = FastDateFormat.getInstance(timeFormat);
/** The audited classes. */
private Set<Class<?>> auditedClasses = new HashSet<>();
/** The Constant DEFAULT_IGNORED_PROPERTIES. */
private static final Set<String> DEFAULT_IGNORED_PROPERTIES = Stream.of("serialVersionUID", "id", "createdDate", "lastModifiedDate", "version", "lastModifiedBy")
.collect(Collectors.toSet());
final TransactionAuditLog log = new TransactionAuditLog();
log.setAction(action);
/** The ignored properties. */
private Set<String> ignoredProperties = new HashSet<>(DEFAULT_IGNORED_PROPERTIES);
log.setClassPk(classPkService.getClassPk(entity.getClass()));
log.setEntityId(id);
log.setPropertyName(propertyName);
log.setPreviousState(previousState);
log.setNewState(currentState);
log.setPreviousObject(previousObject); // Transient
log.setNewObject(currentObject); // Transient
if (referencedEntity != null) {
log.setReferencedEntity(classPkService.getClassPk(referencedEntity));
/** The secured properties of audited entities. */
private final Map<Class<?>, Set<String>> securedClassFields = Collections.synchronizedMap(new HashMap<>());
/** The ignored properties of audited entities. */
private final Map<Class<?>, Set<String>> ignoredClassFields;
public AuditTrailServiceImpl() {
// make synchronized local caches
ignoredClassFields = Collections.synchronizedMap(new HashMap<>());
}
LOG.trace("Creating {} audit log entity={} id={} prop={} old={} new={} ref={}", action, entity.getClass().getName(), id, propertyName, previousState, currentState,
referencedEntity == null ? null : referencedEntity.getName());
/**
* Explicitly set the list of classes that should be audited. Note that any
* class with {@link Audited} annotation will be included, even if not on this
* list.
*
* @param auditedClasses entity classes to audit
* @return
* @see Audited
*/
public Set<Class<?>> setAuditedClasses(final Set<Class<?>> auditedClasses) {
return this.auditedClasses = auditedClasses;
}
return log;
/**
* Gets the audited classes.
*
* @return the audited classes
*/
public Set<Class<?>> getAuditedClasses() {
return auditedClasses;
}
/**
* To audit log.
* Set the list of properties to ignore on all entities (e.g. "password").
* Defaults to {@link #DEFAULT_IGNORED_PROPERTIES}. Note that you can explicitly
* exclude fields by annotating them with <code>@NotAudited</code> annotation
* (see {@link NotAudited}).
*
* @param tlog the tlog
* @return the audit log
* @param ignoredProperties entity property names to exclude from audit trail
* @see NotAudited
*/
private AuditLog toAuditLog(final TransactionAuditLog tlog) {
final AuditLog auditLog = new AuditLog();
auditLog.setLogDate(new Date());
auditLog.setAction(tlog.getAction());
auditLog.setClassPk(tlog.getClassPk());
auditLog.setEntityId(tlog.getEntityId());
auditLog.setNewState(tlog.getNewState());
auditLog.setPreviousState(tlog.getPreviousState());
auditLog.setPropertyName(tlog.getPropertyName());
auditLog.setReferencedEntity(tlog.getReferencedEntity());
auditLog.setPreviousEntity(tlog.getPreviousObject());
auditLog.setNewEntity(tlog.getNewObject());
return auditLog;
public void setIgnoredProperties(final Set<String> ignoredProperties) {
this.ignoredProperties = ignoredProperties;
}
/**
* Gets the ignored properties.
*
* @return the ignored properties
*/
public Set<String> getIgnoredProperties() {
return ignoredProperties;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() throws Exception {
assert ignoredProperties != null;
// Make them unmodifiable
ignoredProperties = Collections.unmodifiableSet(ignoredProperties);
auditedClasses = Collections.unmodifiableSet(auditedClasses);
}
/*
......@@ -179,4 +231,248 @@ public class AuditTrailServiceImpl implements AuditTrailService {
});
return logMap;
}
@Override
@Transactional(propagation = Propagation.MANDATORY)
public List<AuditLog> addAuditLogs(Map<ChangeKey, Map<String, PropertyChange<?>>> currentAuditLogs) {
LOG.debug("Registering changes");
var changeDate = new Date();
var auditLogs = new ArrayList<AuditLog>();
for (Entry<ChangeKey, Map<String, PropertyChange<?>>> entry : currentAuditLogs.entrySet()) {
var key = entry.getKey();
var val = entry.getValue();
final Set<String> entityIgnoredFields = ignoredClassFields.get(key.clazz);
LOG.trace("{} {}#{}", key.type, key.clazz, key.id);
val.forEach((propertyName, change) -> {
if (ignoredProperties.contains(propertyName) || (entityIgnoredFields != null && entityIgnoredFields.contains(propertyName))) {
LOG.trace("{}.{} property is not audited.", key.clazz.getSimpleName(), propertyName);
return;
}
// LOG.warn("\t{} {}: {} -> {}", key.type, propertyName, change.change.getLeft(), change.change.getRight());
var auditLog = toAuditLog(changeDate, key, propertyName, change);
if (auditLog != null) {