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; ...@@ -40,6 +40,7 @@ import org.genesys.blocks.model.EmptyModel;
import org.genesys.blocks.util.JsonSidConverter; import org.genesys.blocks.util.JsonSidConverter;
import org.hibernate.annotations.Type; import org.hibernate.annotations.Type;
import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
...@@ -69,6 +70,7 @@ public class AuditLog extends EmptyModel { ...@@ -69,6 +70,7 @@ public class AuditLog extends EmptyModel {
/** The log date. */ /** The log date. */
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
@Column(name = "logdate", nullable = false) @Column(name = "logdate", nullable = false)
@CreatedDate
private Date logDate; private Date logDate;
/** Class name of the referenced entity. */ /** Class name of the referenced entity. */
...@@ -358,4 +360,14 @@ public class AuditLog extends EmptyModel { ...@@ -358,4 +360,14 @@ public class AuditLog extends EmptyModel {
public Object getNewEntity() { public Object getNewEntity() {
return newEntity; 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 { ...@@ -28,24 +28,6 @@ public class TransactionAuditLog {
/** ID of the referenced entity. */ /** ID of the referenced entity. */
private long entityId; 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. */ /** The action. */
private AuditAction action; private AuditAction action;
...@@ -85,78 +67,6 @@ public class TransactionAuditLog { ...@@ -85,78 +67,6 @@ public class TransactionAuditLog {
this.entityId = entityId; 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. * Sets the action.
* *
...@@ -175,34 +85,6 @@ public class TransactionAuditLog { ...@@ -175,34 +85,6 @@ public class TransactionAuditLog {
return action; 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) * (non-Javadoc)
* @see java.lang.Object#hashCode() * @see java.lang.Object#hashCode()
...@@ -213,7 +95,6 @@ public class TransactionAuditLog { ...@@ -213,7 +95,6 @@ public class TransactionAuditLog {
int result = 1; int result = 1;
result = prime * result + ((classPk == null) ? 0 : classPk.hashCode()); result = prime * result + ((classPk == null) ? 0 : classPk.hashCode());
result = prime * result + (int) (entityId ^ (entityId >>> 32)); result = prime * result + (int) (entityId ^ (entityId >>> 32));
result = prime * result + ((propertyName == null) ? 0 : propertyName.hashCode());
return result; return result;
} }
...@@ -237,20 +118,7 @@ public class TransactionAuditLog { ...@@ -237,20 +118,7 @@ public class TransactionAuditLog {
return false; return false;
if (entityId != other.entityId) if (entityId != other.entityId)
return false; return false;
if (propertyName == null) {
if (other.propertyName != null)
return false;
} else if (!propertyName.equals(other.propertyName))
return false;
return true; 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 @@ ...@@ -15,14 +15,13 @@
*/ */
package org.genesys.blocks.auditlog.service; package org.genesys.blocks.auditlog.service;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; 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.AuditAction;
import org.genesys.blocks.auditlog.model.AuditLog; 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.model.filters.AuditLogFilter;
import org.genesys.blocks.model.EntityId; import org.genesys.blocks.model.EntityId;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
...@@ -44,30 +43,13 @@ public interface AuditTrailService { ...@@ -44,30 +43,13 @@ public interface AuditTrailService {
*/ */
Page<AuditLog> listAuditLogs(AuditLogFilter filters, Pageable page); 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. * Record audit logs in the database.
* *
* @param auditLogs the audit logs * @param currentAuditLogs the audit logs
* @return the list * @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 * List audit logs for the specified entity. Logs are sorted by log date
...@@ -91,4 +73,89 @@ public interface AuditTrailService { ...@@ -91,4 +73,89 @@ public interface AuditTrailService {
*/ */
Map<String, List<AuditLog>> auditLogs(EntityId entity); 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 @@ ...@@ -15,18 +15,35 @@
*/ */
package org.genesys.blocks.auditlog.service.impl; 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.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors; 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.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.AuditAction;
import org.genesys.blocks.auditlog.model.AuditLog; 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.model.filters.AuditLogFilter;
import org.genesys.blocks.auditlog.persistence.AuditLogRepository; import org.genesys.blocks.auditlog.persistence.AuditLogRepository;
import org.genesys.blocks.auditlog.service.AuditTrailService; import org.genesys.blocks.auditlog.service.AuditTrailService;
...@@ -35,13 +52,15 @@ import org.genesys.blocks.model.ClassPK; ...@@ -35,13 +52,15 @@ import org.genesys.blocks.model.ClassPK;
import org.genesys.blocks.model.EntityId; import org.genesys.blocks.model.EntityId;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
/** /**
* Implementation of the {@link AuditTrailService} with JPA. * Implementation of the {@link AuditTrailService} with JPA.
...@@ -50,7 +69,7 @@ import org.springframework.transaction.annotation.Transactional; ...@@ -50,7 +69,7 @@ import org.springframework.transaction.annotation.Transactional;
*/ */
@Service @Service
@Transactional(readOnly = true) @Transactional(readOnly = true)
public class AuditTrailServiceImpl implements AuditTrailService { public class AuditTrailServiceImpl implements AuditTrailService, InitializingBean {
/** The Constant LOG. */ /** The Constant LOG. */
private static final Logger LOG = LoggerFactory.getLogger(AuditTrailServiceImpl.class); private static final Logger LOG = LoggerFactory.getLogger(AuditTrailServiceImpl.class);
...@@ -63,67 +82,100 @@ public class AuditTrailServiceImpl implements AuditTrailService { ...@@ -63,67 +82,100 @@ public class AuditTrailServiceImpl implements AuditTrailService {
@Autowired @Autowired
private AuditLogRepository auditLogRepository; private AuditLogRepository auditLogRepository;
/* /** The date format. */
* (non-Javadoc) private final String dateFormat = "dd-MMM-yyyy";
* @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 time format. */
* (non-Javadoc) private final String timeFormat = "HH:mm:ss";
* @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) {
final TransactionAuditLog log = new TransactionAuditLog(); /** The date time format. */
log.setAction(action); private final String dateTimeFormat = "dd-MMM-yyyy HH:mm:ss";
log.setClassPk(classPkService.getClassPk(entity.getClass())); /** The date formatter. */
log.setEntityId(id); private Format dateFormatter = FastDateFormat.getInstance(dateFormat);
log.setPropertyName(propertyName);
log.setPreviousState(previousState); /** The date time formatter. */
log.setNewState(currentState); private Format dateTimeFormatter = FastDateFormat.getInstance(dateTimeFormat);
log.setPreviousObject(previousObject); // Transient
log.setNewObject(currentObject); // Transient /** The time formatter. */
if (referencedEntity != null) { private Format timeFormatter = FastDateFormat.getInstance(timeFormat);
log.setReferencedEntity(classPkService.getClassPk(referencedEntity));
} /** 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());
/** The ignored properties. */
private Set<String> ignoredProperties = new HashSet<>(DEFAULT_IGNORED_PROPERTIES);
LOG.trace("Creating {} audit log entity={} id={} prop={} old={} new={} ref={}", action, entity.getClass().getName(), id, propertyName, previousState, currentState, /** The secured properties of audited entities. */
referencedEntity == null ? null : referencedEntity.getName()); private final Map<Class<?>, Set<String>> securedClassFields = Collections.synchronizedMap(new HashMap<>());
return log; /** The ignored properties of audited entities. */
private final Map<Class<?>, Set<String>> ignoredClassFields;
public AuditTrailServiceImpl() {
// make synchronized local caches
ignoredClassFields = Collections.synchronizedMap(new HashMap<>());
} }
/** /**
* To audit log. * 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 tlog the tlog * @param auditedClasses entity classes to audit
* @return the audit log * @return
* @see Audited
*/ */
private AuditLog toAuditLog(final TransactionAuditLog tlog) {