Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright 2018 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.blocks.auditlog.component;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.Format;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
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.Set;
import java.util.Stack;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.apache.commons.lang3.StringUtils;
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.service.AuditTrailService;
import org.genesys.blocks.model.BasicModel;
import org.genesys.blocks.model.EntityId;
import org.hibernate.CallbackException;
import org.hibernate.EmptyInterceptor;
import org.hibernate.Transaction;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.resource.transaction.spi.TransactionStatus;
import org.hibernate.type.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ResolvableType;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
/**
* Record changed data using {@link AuditLog} entries.
*
* @author Matija Obreza
*/
@Component
public class AuditTrailInterceptor extends EmptyInterceptor implements InitializingBean {
/** The Constant serialVersionUID. */
private static final long serialVersionUID = 1881637304461659508L;
/** The Constant LOG. */
private static final Logger LOG = LoggerFactory.getLogger(AuditTrailInterceptor.class);
/** The Constant DEFAULT_IGNORED_PROPERTIES. */
private static final Set DEFAULT_IGNORED_PROPERTIES = Stream.of("serialVersionUID", "id", "createdDate", "lastModifiedDate", "version", "lastModifiedBy")
.collect(Collectors.toSet());
/** The ignored properties. */
private Set ignoredProperties = new HashSet<>(DEFAULT_IGNORED_PROPERTIES);
/** The ignored properties of audited entities. */
private final Map, Set> ignoredClassFields;
/** The secured properties of audited entities. */
private final Map, Set> securedClassFields;
/** The audited classes. */
private Set> auditedClasses = new HashSet<>();
/** The included classes. */
// Two caches
private final Set> ignoredClasses, includedClasses;
/** The audit trail service. */
@Autowired
private AuditTrailService auditTrailService;
/** The entity manager. */
@PersistenceContext
private EntityManager entityManager;
/** The date format. */
private final String dateFormat = "dd-MMM-yyyy";
/** 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;
/** The date time formatter. */
private Format dateTimeFormatter;
/** The time formatter. */
private Format timeFormatter;
/** Place to store audit logs before storing them to db */
private static final ThreadLocal>> auditLogStack = new ThreadLocal>>() {
@Override
protected Stack> initialValue() {
return new Stack>();
};
};
/**
* Instantiates a new audit trail interceptor.
*/
public AuditTrailInterceptor() {
LOG.info("Enabling {}", getClass().getName());
// make synchronized local caches
ignoredClasses = Collections.synchronizedSet(new HashSet<>());
includedClasses = Collections.synchronizedSet(new HashSet<>());
ignoredClassFields = Collections.synchronizedMap(new HashMap<>());
securedClassFields = Collections.synchronizedMap(new HashMap<>());
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() throws Exception {
assert ignoredProperties != null;
assert auditTrailService != null;
// Make them unmodifiable
ignoredProperties = Collections.unmodifiableSet(ignoredProperties);
auditedClasses = Collections.unmodifiableSet(auditedClasses);
dateFormatter = FastDateFormat.getInstance(dateFormat);
dateTimeFormatter = FastDateFormat.getInstance(dateTimeFormat);
timeFormatter = FastDateFormat.getInstance(timeFormat);
}
/**
* 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
* @see Audited
*/
public void setAuditedClasses(final Set> auditedClasses) {
this.auditedClasses = auditedClasses;
}
/**
* Gets the audited classes.
*
* @return the audited classes
*/
public Set> getAuditedClasses() {
return auditedClasses;
}
/**
* 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 @NotAudited annotation
* (see {@link NotAudited}).
*
* @param ignoredProperties entity property names to exclude from audit trail
* @see NotAudited
*/
public void setIgnoredProperties(final Set ignoredProperties) {
this.ignoredProperties = ignoredProperties;
}
/**
* Gets the ignored properties.
*
* @return the ignored properties
*/
public Set getIgnoredProperties() {
return ignoredProperties;
}
/**
* Sets the audit trail service.
*
* @param auditTrailService the new audit trail service
*/
public void setAuditTrailService(final AuditTrailService auditTrailService) {
this.auditTrailService = auditTrailService;
}
/**
* Gets the audit trail service.
*
* @return the audit trail service
*/
public AuditTrailService getAuditTrailService() {
return auditTrailService;
}
/*
* (non-Javadoc)
* @see org.hibernate.EmptyInterceptor#onFlushDirty(java.lang.Object,
* java.io.Serializable, java.lang.Object[], java.lang.Object[],
* java.lang.String[], org.hibernate.type.Type[])
*/
/* We add more stuff to the transaction if that fails we're still good! */
@Override
public boolean onFlushDirty(final Object entity, final Serializable id, final Object[] currentState, final Object[] previousState, final String[] propertyNames,
final Type[] types) {
final Class> entityClass = entity.getClass();
if (LOG.isTraceEnabled()) {
LOG.trace("Inspecting Entity.class={} id={}", entityClass, id);
}
if (!isAudited(entityClass)) {
return false;
}
final Set entityIgnoredFields = ignoredClassFields.get(entityClass);
// Identify changed values
for (int i = 0; i < previousState.length; i++) {
final String propertyName = propertyNames[i];
final Object prev = previousState[i];
final Object curr = currentState[i];
if (ignoredProperties.contains(propertyName) || (entityIgnoredFields != null && entityIgnoredFields.contains(propertyName))) {
LOG.trace("{} property in {} is not audited.", propertyName, entityClass.getSimpleName());
continue;
}
if (((prev != null) && !prev.equals(curr)) || ((prev == null) && (curr != null))) {
LOG.trace("prop={} prev={} curr={} type={}", propertyName, prev, curr, types[i].getReturnedClass());
if (isPrimitiveType(types[i].getReturnedClass())) {
final String currentValue = formatValue(curr, types[i], entityClass, propertyName);
final String previousValue = formatValue(prev, types[i], entityClass, propertyName);
// Notice cast to Long here!
recordChange(entity, (Long) id, propertyName, previousValue, currentValue, null, prev, curr);
} else if (isEntity(types[i].getReturnedClass())) {
final EntityId prevEntity = (EntityId) prev, currEntity = (EntityId) curr;
final String previousValue = prevEntity == null ? null : prevEntity.getId().toString();
final String currentValue = currEntity == null ? null : currEntity.getId().toString();
if (!StringUtils.equals(previousValue, currentValue)) {
// Notice cast to Long here!
recordChange(entity, (Long) id, propertyName, previousValue, currentValue, types[i].getReturnedClass(), prev, curr);
}
} else {
LOG.trace("Entity.{} {} is not a primitive. Ignoring value={}", propertyName, prev == null ? null : prev.getClass(), prev);
// TODO Capture in audit log
}
}
}
return false;
}
/**
* Format value.
*
* @param someValue the some value
* @param type the type
* @param entityClass the entity class
* @param propertyName the property name
* @return the string
*/
private String formatValue(final Object someValue, final Type type, final Class> entityClass, final String propertyName) {
if (someValue == null) {
return null;
}
// Check if field should be masked
final Set securedFields = securedClassFields.get(entityClass);
if (securedFields != null && securedFields.contains(propertyName)) {
return AuditLog.FIELD_VALUE_NOT_AUDITED;
}
final Class> returnedClass = type.getReturnedClass();
if (Instant.class.equals(returnedClass)) {
return DateTimeFormatter.ISO_DATE_TIME.withZone(ZoneOffset.systemDefault()).format((Instant)someValue);
} else if (LocalDate.class.equals(returnedClass)) {
return DateTimeFormatter.ISO_DATE.format((LocalDate)someValue);
} else if (Date.class.equals(returnedClass) || Calendar.class.equals(returnedClass)) {
TemporalType temporalType = TemporalType.TIMESTAMP;
try {
final Field field = entityClass.getDeclaredField(propertyName);
if ((field != null) && field.isAnnotationPresent(Temporal.class)) {
final Temporal ta = field.getAnnotation(Temporal.class);
temporalType = ta.value();
}
} catch (NoSuchFieldException | SecurityException e) {
LOG.trace("Could not access field {}#{}", entityClass, propertyName);
}
switch (temporalType) {
case TIMESTAMP:
return dateTimeFormatter.format(someValue);
case DATE:
return dateFormatter.format(someValue);
case TIME:
return timeFormatter.format(someValue);
}
}
return someValue.toString();
}
/**
* Checks if is entity.
*
* @param clazz the clazz
* @return true, if is entity
*/
private boolean isEntity(final Class> clazz) {
if (EntityId.class.isAssignableFrom(clazz)) {
return true;
}
LOG.trace("{} is not an EntityId", clazz.getName());
return false;
}
/*
* (non-Javadoc)
* @see org.hibernate.EmptyInterceptor#onDelete(java.lang.Object,
* java.io.Serializable, java.lang.Object[], java.lang.String[],
* org.hibernate.type.Type[])
*/
@Override
public void onDelete(final Object entity, final Serializable id, final Object[] states, final String[] propertyNames, final Type[] types) {
final Class> entityClass = entity.getClass();
if (LOG.isTraceEnabled()) {
LOG.trace("Inspecting Entity.class={} id={}", entityClass, id);
}
if (!isAudited(entityClass)) {
LOG.trace("{} is not audited", entityClass);
return;
}
final Set entityIgnoredFields = ignoredClassFields.get(entityClass);
for (int i = 0; i < states.length; i++) {
final String propertyName = propertyNames[i];
final Object state = states[i];
if (ignoredProperties.contains(propertyName) || (entityIgnoredFields != null && entityIgnoredFields.contains(propertyName))) {
continue;
}
if (state != null) {
LOG.trace("Deleted prop={} state={} type={}", propertyName, state, types[i].getReturnedClass());
if (isPrimitiveType(types[i].getReturnedClass())) {
// Notice cast to Long here!
recordDelete(entity, (Long) id, propertyName, state.toString(), null, null);
} else if (isEntity(types[i].getReturnedClass())) {
final EntityId prevEntity = (EntityId) state;
final String previousValue = prevEntity == null ? null : prevEntity.getId().toString();
// Notice cast to Long here!
recordDelete(entity, (Long) id, propertyName, previousValue, types[i].getReturnedClass(), state);
} else {
LOG.trace("Entity.{} {} is not a primitive. Ignoring value={}", propertyName, state == null ? null : state.getClass(), state);
// TODO Capture in audit log
// PersistentBag
}
}
}
}
/*
* (non-Javadoc)
* @see org.hibernate.EmptyInterceptor#onCollectionRecreate(java.lang.Object,
* java.io.Serializable)
*/
@Override
public void onCollectionRecreate(final Object collection, final Serializable key) throws CallbackException {
LOG.trace("Collection recreated: key={} coll={}", key, collection);
}
/*
* (non-Javadoc)
* @see org.hibernate.EmptyInterceptor#onCollectionRemove(java.lang.Object,
* java.io.Serializable)
*/
@Override
public void onCollectionRemove(final Object collection, final Serializable key) throws CallbackException {
final PersistentCollection pc = (PersistentCollection) collection;
if (!isAudited(pc.getOwner().getClass())) {
LOG.trace("Class {} is not audited", pc.getOwner().getClass());
return;
}
final Class extends Object> ownerClass = pc.getOwner().getClass();
final String propertyName = pc.getRole().substring(pc.getRole().lastIndexOf('.') + 1);
final Class> propertyType = findPropertyType(ownerClass, propertyName);
LOG.trace("Property class: {}.{}={}", ownerClass.getName(), propertyName, propertyType);
Collection