Commit 454b969e authored by Matija Obreza's avatar Matija Obreza
Browse files

Initial commit

parents
.settings/
.classpath
.project
pom.xml.releaseBackup
release.properties
npm-debug.log
bower_components/
*.iml
target/
.idea/
effective.pom
node_modules/
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2017 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.genesys-pgr</groupId>
<artifactId>application-blocks-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../parent/pom.xml</relativePath>
</parent>
<artifactId>application-blocks-auditlog</artifactId>
<packaging>jar</packaging>
<name>JPA Audit Log</name>
<description>Records changes to entites</description>
<properties>
<deploy.artifact>true</deploy.artifact>
</properties>
<build>
<plugins>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<includes>
<!-- List packages to be processed -->
<include>org.genesys.blocks.auditlog.model</include>
</includes>
<outputDirectory>target/generated-sources/querydsl</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>4.1.4</version>
</dependency>
</dependencies>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
<resource>
<directory>${project.build.directory}/generated-resources</directory>
<filtering>false</filtering>
</resource>
</resources>
</build>
<dependencies>
<dependency>
<groupId>org.genesys-pgr</groupId>
<artifactId>application-blocks-security</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.genesys-pgr</groupId>
<artifactId>application-blocks-core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
<version>7.0.42</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>${hsqldb.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${hibernate.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>${spring.data.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>${querydsl.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
<version>${hibernate.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-acl</artifactId>
<version>${spring.security.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>${spring.security.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
/*
* Copyright 2017 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.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Add this annotation to persisted entities to explicitly include them in auditing.
*
* @author Matija Obreza
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@Inherited
public @interface Audited {
}
/*
* Copyright 2017 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.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Add this annotation to persisted fields to exclude them from audit logging.
*
* @author Matija Obreza
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.TYPE })
public @interface NotAudited {
}
/*
* Copyright 2017 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.Method;
import java.lang.reflect.ParameterizedType;
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.HashSet;
import java.util.Map;
import java.util.Set;
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.NotAudited;
import org.genesys.blocks.auditlog.model.AuditLog;
import org.genesys.blocks.auditlog.service.AuditTrailService;
import org.genesys.blocks.model.EntityId;
import org.hibernate.CallbackException;
import org.hibernate.EmptyInterceptor;
import org.hibernate.collection.spi.PersistentCollection;
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.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 {
private static final long serialVersionUID = 1881637304461659508L;
private static final Logger LOG = LoggerFactory.getLogger(AuditTrailInterceptor.class);
private static final Set<String> DEFAULT_IGNORED_PROPERTIES = Stream.of("serialVersionUID", "id", "password", "createdDate", "lastModifiedDate", "version", "lastModifiedBy")
.collect(Collectors.toSet());
private Set<String> ignoredProperties = new HashSet<>(DEFAULT_IGNORED_PROPERTIES);
private Set<Class<?>> auditedClasses = new HashSet<>();
// Two caches
private final Set<Class<?>> ignoredClasses, includedClasses;
@Autowired
private AuditTrailService auditTrailService;
@PersistenceContext
private EntityManager entityManager;
private String dateFormat = "dd-MMM-yyyy";
private String timeFormat = "HH:mm:ss";
private String dateTimeFormat = "dd-MMM-yyyy HH:mm:ss";
private Format dateFormatter;
private Format dateTimeFormatter;
private Format timeFormatter;
public AuditTrailInterceptor() {
// make synchronized local caches
ignoredClasses = Collections.synchronizedSet(new HashSet<>());
includedClasses = Collections.synchronizedSet(new HashSet<>());
}
/*
* (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<Class<?>> auditedClasses) {
this.auditedClasses = auditedClasses;
}
/**
* Gets the audited classes.
*
* @return the audited classes
*/
public Set<Class<?>> 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
* <code>@NotAudited</code> annotation (see {@link NotAudited}).
*
* @param ignoredProperties entity property names to exclude from audit trail
* @see NotAudited
*/
public void setIgnoredProperties(final Set<String> ignoredProperties) {
this.ignoredProperties = ignoredProperties;
}
/**
* Gets the ignored properties.
*
* @return the ignored properties
*/
public Set<String> 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 Date logDate = new Date();
// 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)) {
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);
if (!StringUtils.equals(previousValue, currentValue)) {
// Notice cast to Long here!
recordChange(logDate, entity, (Long) id, propertyName, previousValue, currentValue, null);
}
} 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(logDate, entity, (Long) id, propertyName, previousValue, currentValue, types[i].getReturnedClass());
}
} else {
LOG.warn("Entity.{} {} is not a primitive. Ignoring value={}", propertyName, prev == null ? null : prev.getClass(), prev);
// TODO Capture in audit log
}
}
}
return false;
}
private String formatValue(Object someValue, Type type, Class<?> entityClass, String propertyName) {
if (someValue == null) {
return null;
}
Class<?> returnedClass = type.getReturnedClass();
if (Date.class.equals(returnedClass) || Calendar.class.equals(returnedClass)) {
TemporalType temporalType = TemporalType.TIMESTAMP;
try {
Field field = entityClass.getDeclaredField(propertyName);
if (field != null && field.isAnnotationPresent(Temporal.class)) {
Temporal ta = field.getAnnotation(Temporal.class);
temporalType = ta.value();
}
} catch (NoSuchFieldException | SecurityException e) {
LOG.warn("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();
}
private boolean isEntity(final Class<?> clazz) {
if (EntityId.class.isAssignableFrom(clazz)) {
return true;
}
LOG.debug("{} is not an EntityId", clazz.getName());
return false;
}
@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)) {
return;
}
final Date logDate = new Date();
for (int i = 0; i < states.length; i++) {
final String propertyName = propertyNames[i];
final Object state = states[i];
if (ignoredProperties.contains(propertyName)) {
continue;
}
if (state != null) {
LOG.debug("Deleted prop={} state={} type={}", propertyName, state, types[i].getReturnedClass());
if (isPrimitiveType(types[i].getReturnedClass())) {
// Notice cast to Long here!
recordDelete(logDate, entity, (Long) id, propertyName, state == null ? null : state.toString(), 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(logDate, entity, (Long) id, propertyName, previousValue, types[i].getReturnedClass());
} else {
LOG.trace("Entity.{} {} is not a primitive. Ignoring value={}", propertyName, state == null ? null : state.getClass(), state);
// TODO Capture in audit log
// PersistentBag
}
}
}
}
@Override
public void onCollectionRecreate(final Object collection, final Serializable key) throws CallbackException {
LOG.debug("Collection recreated: key={} coll={}", key, collection);
}
@Override
public void onCollectionRemove(final Object collection, final Serializable key) throws CallbackException {
final PersistentCollection pc = (PersistentCollection) collection;
if (!isAudited(pc.getOwner().getClass())) {
return;
}
LOG.debug("Collection remove: key={} coll={}", key, collection);
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<Object> deleted = new HashSet<>();
final Date logDate = new Date();
if (pc.getValue() instanceof Collection<?>) {