From ab4fdf7010eee40359cdb97bbce34fdbcef16157 Mon Sep 17 00:00:00 2001 From: Dennie de Lange Date: Wed, 21 Feb 2018 09:43:57 +0100 Subject: [PATCH] Split Stampable to allow different timestamp implements (java8 localdatetime) for example. Introduced plugin.yml instead of DefaultAuditLogConfig. Added initial unit test --- .../grails-app/domain/test/Coach.groovy | 4 +- .../grails-app/domain/test/Train.groovy | 5 +- plugin/build.gradle | 2 +- .../conf/DefaultAuditLogConfig.groovy | 33 ---- plugin/grails-app/conf/plugin.yml | 19 +++ .../auditable/AuditLoggingConfigUtils.groovy | 115 +------------- .../orm/auditable/ReflectionUtils.groovy | 84 ----------- .../plugins/orm/auditable/StampActor.groovy | 6 + .../orm/auditable/StampAutoTimestamp.groovy | 6 + .../orm/auditable/StampListener.groovy | 34 ++--- .../plugins/orm/auditable/Stampable.groovy | 16 -- .../orm/auditable/AuditLogListenerSpec.groovy | 53 +++++++ .../orm/auditable/StampListenerSpec.groovy | 64 ++++++++ .../orm/auditable/domain/Airplane.groovy | 14 ++ .../orm/auditable/domain/AuditTrail.groovy | 141 ++++++++++++++++++ .../orm/auditable/domain/Person.groovy | 10 ++ 16 files changed, 340 insertions(+), 266 deletions(-) delete mode 100644 plugin/grails-app/conf/DefaultAuditLogConfig.groovy create mode 100644 plugin/grails-app/conf/plugin.yml delete mode 100644 plugin/src/main/groovy/grails/plugins/orm/auditable/ReflectionUtils.groovy create mode 100644 plugin/src/main/groovy/grails/plugins/orm/auditable/StampActor.groovy create mode 100644 plugin/src/main/groovy/grails/plugins/orm/auditable/StampAutoTimestamp.groovy delete mode 100644 plugin/src/main/groovy/grails/plugins/orm/auditable/Stampable.groovy create mode 100644 plugin/src/test/groovy/grails/plugins/orm/auditable/AuditLogListenerSpec.groovy create mode 100644 plugin/src/test/groovy/grails/plugins/orm/auditable/StampListenerSpec.groovy create mode 100644 plugin/src/test/groovy/grails/plugins/orm/auditable/domain/Airplane.groovy create mode 100644 plugin/src/test/groovy/grails/plugins/orm/auditable/domain/AuditTrail.groovy create mode 100644 plugin/src/test/groovy/grails/plugins/orm/auditable/domain/Person.groovy diff --git a/examples/audit-test/grails-app/domain/test/Coach.groovy b/examples/audit-test/grails-app/domain/test/Coach.groovy index 010d5d8c..7b1b8f4d 100644 --- a/examples/audit-test/grails-app/domain/test/Coach.groovy +++ b/examples/audit-test/grails-app/domain/test/Coach.groovy @@ -1,8 +1,8 @@ package test -import grails.plugins.orm.auditable.Stampable +import grails.plugins.orm.auditable.StampActor -class Coach implements Stampable { +class Coach implements StampActor { static constraints = { } diff --git a/examples/audit-test/grails-app/domain/test/Train.groovy b/examples/audit-test/grails-app/domain/test/Train.groovy index 9bfc26c6..899876be 100644 --- a/examples/audit-test/grails-app/domain/test/Train.groovy +++ b/examples/audit-test/grails-app/domain/test/Train.groovy @@ -1,8 +1,9 @@ package test -import grails.plugins.orm.auditable.Stampable +import grails.plugins.orm.auditable.StampActor +import grails.plugins.orm.auditable.StampAutoTimestamp -class Train implements Stampable { +class Train implements StampActor, StampAutoTimestamp{ String number static constraints = { diff --git a/plugin/build.gradle b/plugin/build.gradle index 70e14375..0622602c 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -61,7 +61,7 @@ dependencies { provided "org.grails.plugins:spring-security-core:3.2.0" testCompile "org.grails:grails-gorm-testing-support" - testCompile "org.grails:grails-plugin-testing" + testCompile "org.grails:grails-web-testing-support" } bootRepackage.enabled = false diff --git a/plugin/grails-app/conf/DefaultAuditLogConfig.groovy b/plugin/grails-app/conf/DefaultAuditLogConfig.groovy deleted file mode 100644 index df0ca654..00000000 --- a/plugin/grails-app/conf/DefaultAuditLogConfig.groovy +++ /dev/null @@ -1,33 +0,0 @@ -/* Copyright 2006-2015 the original author or authors. - * - * 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. - */ - -// Default Plugin configuration -defaultAuditLog { - auditDomainClassName = null - - disabled = false - verbose = true - failOnError = false - logIds = true - logFullClassName = true - excluded = ['version', 'lastUpdated', 'lastUpdatedBy'] - included = null - mask = ['password'] - propertyMask = "**********" - defaultActor = 'SYS' - - // Enable support for Stampable - stampEnabled = true -} diff --git a/plugin/grails-app/conf/plugin.yml b/plugin/grails-app/conf/plugin.yml new file mode 100644 index 00000000..875c32e1 --- /dev/null +++ b/plugin/grails-app/conf/plugin.yml @@ -0,0 +1,19 @@ +grails: + plugin: + auditLog: + auditDomainClassName: null + disabled: false + verbose: true + failOnError: false + logIds: true + logFullClassName: true + excluded: + - version + - lastUpdated + - lastUpdatedBy + included: null + mask: + - password + propertyMask: "**********" + defaultActor: 'SYS' + stampEnabled: true diff --git a/plugin/src/main/groovy/grails/plugins/orm/auditable/AuditLoggingConfigUtils.groovy b/plugin/src/main/groovy/grails/plugins/orm/auditable/AuditLoggingConfigUtils.groovy index 51fa89e2..aa87ac65 100644 --- a/plugin/src/main/groovy/grails/plugins/orm/auditable/AuditLoggingConfigUtils.groovy +++ b/plugin/src/main/groovy/grails/plugins/orm/auditable/AuditLoggingConfigUtils.groovy @@ -14,8 +14,7 @@ */ package grails.plugins.orm.auditable -import grails.core.GrailsApplication -import grails.util.Environment +import grails.util.Holders import groovy.util.logging.Slf4j /** @@ -25,118 +24,12 @@ import groovy.util.logging.Slf4j */ @Slf4j class AuditLoggingConfigUtils { - private static ConfigObject _auditConfig - private static GrailsApplication application - - // Constructor. Static methods only - private AuditLoggingConfigUtils() {} - - /** - * Parse and load the auditLog configuration. - * @return the configuration - */ - static synchronized ConfigObject getAuditConfig() { - if (_auditConfig == null) { - log.trace 'Building auditLog config since there is no cached config' - reloadAuditConfig() - } - _auditConfig - } - - /** - * For testing only. - * @param config the config - */ - static void setAuditConfig(ConfigObject config) { - _auditConfig = config - } - - /** Reset the config for testing or after a dev mode Config.groovy change. */ - static synchronized void resetAuditConfig() { - _auditConfig = null - log.trace 'reset auditLog config' - } - - /** - * Allow a secondary plugin to add config attributes. - * @param className the name of the config class. - */ - static synchronized void loadSecondaryConfig(String className) { - mergeConfig auditConfig, className - log.trace 'loaded secondary config {}', className - } + static @Lazy + ConfigObject auditConfig = { Holders.config.grails.plugin.auditLog }() /** Force a reload of the auditLog configuration. */ static void reloadAuditConfig() { - mergeConfig ReflectionUtils.auditConfig, 'DefaultAuditLogConfig' + auditConfig = null log.trace 'reloaded auditLog config' } - - /** - * Merge in a secondary config (provided by plugin as defaults) into the main config. - * @param currentConfig the current configuration - * @param className the name of the config class to load - */ - private static void mergeConfig(ConfigObject currentConfig, String className) { - log.trace("Merging currentConfig with $className") - GroovyClassLoader classLoader = new GroovyClassLoader(Thread.currentThread().contextClassLoader) - ConfigObject secondary = new ConfigSlurper(Environment.current.name).parse(classLoader.loadClass(className)) - secondary = secondary.defaultAuditLog as ConfigObject - - Collection keysToDefaultEmpty = [] - findKeysToDefaultEmpty secondary, '', keysToDefaultEmpty - - ConfigObject merged = mergeConfig(currentConfig, secondary) - - // having discovered the keys that have map values (since they initially point to empty maps), - // check them again and remove the damage done when Map values are 'flattened' - for (String key in keysToDefaultEmpty) { - Map value = (Map) ReflectionUtils.getConfigProperty(key, merged) - for (Iterator iter = value.entrySet().iterator(); iter.hasNext();) { - def entry = iter.next() - if (entry.value instanceof Map) { - iter.remove() - } - } - } - - _auditConfig = ReflectionUtils.auditConfig = merged - } - - /** - * Merge two configs together. The order is important if secondary is not null then - * start with that and merge the main config on top of that. This lets the secondary - * config act as default values but let user-supplied values in the main config override them. - * - * @param currentConfig the main config, starting from Config.groovy - * @param secondary new default values - * @return the merged configs - */ - private static ConfigObject mergeConfig(ConfigObject currentConfig, ConfigObject secondary) { - log.trace("Merging secondary config on top of currentConfig") - (secondary ?: new ConfigObject()).merge(currentConfig ?: new ConfigObject()) as ConfigObject - } - - /** - * Given an unmodified config map with defaults, loop through the keys looking for values that are initially - * empty maps. This will be used after merging to remove map values that cause problems by being included both as - * the result from the ConfigSlurper (which is correct) and as a "flattened" maps which confuse Audit Logging. - * @param m the config map - * @param fullPath the path to this config map, e.g. 'grails.plugin.auditLog - * @param keysToDefaultEmpty a collection of key names to add to - */ - private static void findKeysToDefaultEmpty(Map m, String fullPath, Collection keysToDefaultEmpty) { - m.each { k, v -> - if (v instanceof Map) { - if (v) { - // recurse - findKeysToDefaultEmpty((Map) v, fullPath + '.' + k, keysToDefaultEmpty) - } - else { - // since it's an empty map, capture its path for the cleanup pass - keysToDefaultEmpty << (fullPath + '.' + k).substring(1) - } - } - } - } } diff --git a/plugin/src/main/groovy/grails/plugins/orm/auditable/ReflectionUtils.groovy b/plugin/src/main/groovy/grails/plugins/orm/auditable/ReflectionUtils.groovy deleted file mode 100644 index 68c7766d..00000000 --- a/plugin/src/main/groovy/grails/plugins/orm/auditable/ReflectionUtils.groovy +++ /dev/null @@ -1,84 +0,0 @@ -/* Copyright 2006-2015 the original author or authors. - * - * 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 grails.plugins.orm.auditable - -import grails.core.GrailsApplication -import grails.util.Holders -import groovy.util.logging.Slf4j -import org.grails.config.PropertySourcesConfig -import org.springframework.core.env.MapPropertySource -import org.springframework.core.env.PropertySource - -/** - * Helper methods that use dynamic Groovy - */ -@Slf4j -class ReflectionUtils { - - // set at startup - static GrailsApplication application - - private ReflectionUtils() { - // static only - } - - static Object getConfigProperty(String name, config = AuditLoggingConfigUtils.auditConfig) { - def value = config - name.split('\\.').each { String part -> value = value."$part" } - value - } - - static void setConfigProperty(String name, value) { - def config = AuditLoggingConfigUtils.auditConfig - - List parts = name.split('\\.') - name = parts.remove(parts.size() - 1) - - parts.each { String part -> config = config."$part" } - - config."$name" = value - } - - static List asList(Object o) { - o ? o as List : [] - } - - static ConfigObject getAuditConfig() { - def grailsConfig = getApplication().config - if (grailsConfig.auditLog) { - log.error "Your auditLog configuration settings use the old prefix 'auditLog' but must now use 'grails.plugin.auditLog'" - } - grailsConfig.grails.plugin.auditLog - } - - static void setAuditConfig(ConfigObject c) { - ConfigObject config = new ConfigObject() - config.grails.plugin.auditLog = c - - PropertySource propertySource = new MapPropertySource('AuditConfig', [:] << config) - def propertySources = application.mainContext.environment.propertySources - propertySources.addFirst propertySource - - getApplication().config = new PropertySourcesConfig(propertySources) - } - - private static GrailsApplication getApplication() { - if (!application) { - application = Holders.grailsApplication - } - application - } - -} diff --git a/plugin/src/main/groovy/grails/plugins/orm/auditable/StampActor.groovy b/plugin/src/main/groovy/grails/plugins/orm/auditable/StampActor.groovy new file mode 100644 index 00000000..93f25f17 --- /dev/null +++ b/plugin/src/main/groovy/grails/plugins/orm/auditable/StampActor.groovy @@ -0,0 +1,6 @@ +package grails.plugins.orm.auditable + +trait StampActor { + String createdBy = "N/A" + String lastUpdatedBy = "N/A" +} \ No newline at end of file diff --git a/plugin/src/main/groovy/grails/plugins/orm/auditable/StampAutoTimestamp.groovy b/plugin/src/main/groovy/grails/plugins/orm/auditable/StampAutoTimestamp.groovy new file mode 100644 index 00000000..591b4b2c --- /dev/null +++ b/plugin/src/main/groovy/grails/plugins/orm/auditable/StampAutoTimestamp.groovy @@ -0,0 +1,6 @@ +package grails.plugins.orm.auditable + +trait StampAutoTimestamp { + Date dateCreated + Date lastUpdated +} diff --git a/plugin/src/main/groovy/grails/plugins/orm/auditable/StampListener.groovy b/plugin/src/main/groovy/grails/plugins/orm/auditable/StampListener.groovy index de634434..be531fd0 100755 --- a/plugin/src/main/groovy/grails/plugins/orm/auditable/StampListener.groovy +++ b/plugin/src/main/groovy/grails/plugins/orm/auditable/StampListener.groovy @@ -23,11 +23,8 @@ import grails.plugins.orm.auditable.resolvers.AuditRequestResolver import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import org.grails.datastore.mapping.core.Datastore -import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent -import org.grails.datastore.mapping.engine.event.AbstractPersistenceEventListener -import org.grails.datastore.mapping.engine.event.EventType -import org.grails.datastore.mapping.engine.event.PreInsertEvent -import org.grails.datastore.mapping.engine.event.PreUpdateEvent +import org.grails.datastore.mapping.engine.EntityAccess +import org.grails.datastore.mapping.engine.event.* import org.springframework.context.ApplicationEvent /** @@ -39,6 +36,10 @@ import org.springframework.context.ApplicationEvent class StampListener extends AbstractPersistenceEventListener { private GrailsApplication grailsApplication + static final String CREATED_BY = "createdBy" + static final String LAST_UPDATED_BY = "lastUpdatedBy" + + StampListener(Datastore datastore, GrailsApplication grailsApplication) { super(datastore) this.grailsApplication = grailsApplication @@ -50,7 +51,7 @@ class StampListener extends AbstractPersistenceEventListener { log.trace("Event received for datastore {}, ignoring", event.source) return } - if (!(event.entityObject instanceof Stampable)) { + if (!(event.entityObject instanceof StampActor)) { return } @@ -60,27 +61,26 @@ class StampListener extends AbstractPersistenceEventListener { } try { - Stampable domain = event.entityObject as Stampable + StampActor domain = event.entityObject as StampActor log.trace("Stamping object {}", event.entityObject.class.name) // Lookup the request resolver here to ensure that applications have a chance // to override this bean to provide different strategies AuditRequestResolver requestResolver = grailsApplication.mainContext.getBean(AuditRequestResolver) - switch(event.eventType) { + switch (event.eventType) { case EventType.PreInsert: - handleInsert(domain, requestResolver) + handleInsert(event.entityAccess, requestResolver) break case EventType.PreUpdate: - handleUpdate(domain, requestResolver) + handleUpdate(event.entityAccess, requestResolver) break } } catch (Exception e) { if (AuditLogContext.context.failOnError) { throw e - } - else { + } else { log.error("Error stamping domain ${event.entityObject}", e) } } @@ -94,17 +94,17 @@ class StampListener extends AbstractPersistenceEventListener { /** * Stamp inserts */ - protected void handleInsert(Stampable domain, AuditRequestResolver requestResolver) { + protected void handleInsert(EntityAccess entityAccess, AuditRequestResolver requestResolver) { // Set actors, Grails will take care of setting the dates String currentActor = requestResolver.currentActor - domain.createdBy = currentActor - domain.lastUpdatedBy = currentActor + entityAccess.setProperty(CREATED_BY, currentActor) + entityAccess.setProperty(LAST_UPDATED_BY, currentActor) } /** * Stamp updates */ - protected void handleUpdate(Stampable domain, AuditRequestResolver requestResolver) { - domain.lastUpdatedBy = requestResolver.currentActor + protected void handleUpdate(EntityAccess entityAccess, AuditRequestResolver requestResolver) { + entityAccess.setProperty(LAST_UPDATED_BY, requestResolver.currentActor) } } diff --git a/plugin/src/main/groovy/grails/plugins/orm/auditable/Stampable.groovy b/plugin/src/main/groovy/grails/plugins/orm/auditable/Stampable.groovy deleted file mode 100644 index 2c80da8f..00000000 --- a/plugin/src/main/groovy/grails/plugins/orm/auditable/Stampable.groovy +++ /dev/null @@ -1,16 +0,0 @@ -package grails.plugins.orm.auditable - -import org.grails.datastore.gorm.GormEntity - -/** - * Entities should implement this trait to provide automatic stamping of date and user information - */ -trait Stampable extends GormEntity { - // Grails will automatically populate these - Date dateCreated - Date lastUpdated - - // We initialize these to non-null to they pass initial validation, they are set on insert/update - String createdBy = "N/A" - String lastUpdatedBy = "N/A" -} diff --git a/plugin/src/test/groovy/grails/plugins/orm/auditable/AuditLogListenerSpec.groovy b/plugin/src/test/groovy/grails/plugins/orm/auditable/AuditLogListenerSpec.groovy new file mode 100644 index 00000000..5b7a806b --- /dev/null +++ b/plugin/src/test/groovy/grails/plugins/orm/auditable/AuditLogListenerSpec.groovy @@ -0,0 +1,53 @@ +package grails.plugins.orm.auditable + +import grails.plugins.orm.auditable.domain.Airplane +import grails.plugins.orm.auditable.domain.AuditTrail +import grails.plugins.orm.auditable.resolvers.DefaultAuditRequestResolver +import grails.testing.gorm.DataTest +import spock.lang.Specification + +class AuditLogListenerSpec extends Specification implements DataTest { + + @Override + Class[] getDomainClassesToMock() { + [AuditTrail, Airplane] as Class[] + } + + void setupSpec() { + applicationContext.addApplicationListener( + new AuditLogListener(dataStore, grailsApplication) + ) + } + + @Override + Closure doWithConfig() { + return { config -> + config.grails.plugin.auditLog.auditDomainClassName = 'grails.plugins.orm.auditable.domain.AuditTrail' + config.grails.plugin.auditLog.defaultActor = 'SYS' + config.grails.plugin.auditLog.verbose = true + config.grails.plugin.auditLog.excluded = ['version'] + } + } + + @Override + Closure doWithSpring() { + return { + auditRequestResolver(DefaultAuditRequestResolver) + } + } + + void 'should store audit events on insert'() { + given: + Airplane airplane = new Airplane( + make: 'Airbus', + number: '1' + ) + airplane.save(flush: true) + expect: + AuditTrail.count() == 2 + } + void 'should rollback store audit events on insert'() { + expect: 'implement me' + true + } +} diff --git a/plugin/src/test/groovy/grails/plugins/orm/auditable/StampListenerSpec.groovy b/plugin/src/test/groovy/grails/plugins/orm/auditable/StampListenerSpec.groovy new file mode 100644 index 00000000..13988981 --- /dev/null +++ b/plugin/src/test/groovy/grails/plugins/orm/auditable/StampListenerSpec.groovy @@ -0,0 +1,64 @@ +package grails.plugins.orm.auditable + +import grails.plugins.orm.auditable.domain.Person +import grails.plugins.orm.auditable.resolvers.DefaultAuditRequestResolver +import grails.testing.gorm.DataTest +import spock.lang.Specification + +class StampListenerSpec extends Specification implements DataTest { + + void setupSpec() { + applicationContext.addApplicationListener( + new StampListener(dataStore, grailsApplication) + ) + } + + @Override + Class[] getDomainClassesToMock() { + [Person] as Class[] + } + + @Override + Closure doWithSpring() { + return { + auditRequestResolver(DefaultAuditRequestResolver) + } + } + + @Override + Closure doWithConfig() { + return { config -> + config.grails.plugin.auditLog.stampEnabled = true + config.grails.plugin.auditLog.defaultActor = 'SYS' + } + } + + void 'should stamp createdBy and lastUpdatedBy on save'() { + given: + Person p = new Person(name: 'tkvw') + p.save(flush: true) + expect: + p.createdBy == 'SYS' + p.lastUpdatedBy == 'SYS' + } + + void 'should stamp lastUpdatedBy on update'() { + given: + Person p = new Person(name: 'tkvw') + p.save(flush: true) + expect: + p.createdBy == 'SYS' + p.lastUpdatedBy == 'SYS' + when: + AuditLogContext.withConfig(defaultActor: 'foo') { + p.name = 'other' + p.save(flush: true) + } + then: + p.createdBy == 'SYS' + p.lastUpdatedBy == 'foo' + + } + +} + diff --git a/plugin/src/test/groovy/grails/plugins/orm/auditable/domain/Airplane.groovy b/plugin/src/test/groovy/grails/plugins/orm/auditable/domain/Airplane.groovy new file mode 100644 index 00000000..8d37270a --- /dev/null +++ b/plugin/src/test/groovy/grails/plugins/orm/auditable/domain/Airplane.groovy @@ -0,0 +1,14 @@ +package grails.plugins.orm.auditable.domain + +import grails.gorm.annotation.Entity +import grails.plugins.orm.auditable.Auditable + +@Entity +class Airplane implements Auditable{ + String make + String number + + static constraints = { + make nullable: true + } +} diff --git a/plugin/src/test/groovy/grails/plugins/orm/auditable/domain/AuditTrail.groovy b/plugin/src/test/groovy/grails/plugins/orm/auditable/domain/AuditTrail.groovy new file mode 100644 index 00000000..7ad2370f --- /dev/null +++ b/plugin/src/test/groovy/grails/plugins/orm/auditable/domain/AuditTrail.groovy @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 grails.plugins.orm.auditable.domain + +import grails.gorm.annotation.Entity +import groovy.transform.ToString + +/** + * AuditTrails are reported to the AuditLog table. + * This requires you to set up a table or allow + * Grails to create a table for you. (e.g. DDL or db-migration plugin) + */ +@Entity +@ToString(includes = 'id,className,actor,propertyName,oldValue,newValue') +class AuditTrail implements Serializable { + private static final long serialVersionUID = 1L + + String id + Date dateCreated + Date lastUpdated + + String actor + String uri + String className + String persistedObjectId + Long persistedObjectVersion = 0 + + String eventName + String propertyName + String oldValue + String newValue + + static constraints = { + actor(nullable: true) + uri(nullable: true) + className(nullable: true) + persistedObjectId(nullable: true) + persistedObjectVersion(nullable: true) + eventName(nullable: true) + propertyName(nullable: true) + + oldValue(nullable: true) + newValue(maxSize: 10000, nullable: true) + + // for large column support (as in < 1.0.6 plugin versions), use + // oldValue(nullable: true, maxSize: 65534) + // newValue(nullable: true, maxSize: 65534) + } + + static mapping = { + + table 'audit_log' + + cache usage: 'read-only', include: 'non-lazy' + + // Set similiar when you used "auditLog.useDatasource" in < 1.1.0 plugin version. + // datasource "yourdatasource" +// datasource 'second' + + // no HQL queries package name import (was default in 1.x version) + //autoImport false + + // Since 2.0.0, mapping is not done by config anymore. Configure your ID mapping here. + id generator: "uuid2", type: 'string', length: 36 + + version false + } + + static namedQueries = { + forQuery { String q -> + if (!q?.trim()) return // return all + def queries = q.tokenize()?.collect { + '%' + it.replaceAll('_', '\\\\_') + '%' + } + queries.each { query -> + or { + ilike 'actor', query + ilike 'persistedObjectId', query + ilike 'propertyName', query + ilike 'oldValue', query + ilike 'newValue', query + } + } + } + + forDateCreated { Date date -> + if (!date) return + gt 'dateCreated', date + } + } + + /** + * Deserializer that maps a stored map onto the object + * assuming that the keys match attribute properties. + */ + private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { + def map = input.readObject() + map.each { k, v -> this."$k" = v } + } + + /** + * Because Closures do not serialize we can't send the constraints closure + * to the Serialize API so we have to have a custom serializer to allow for + * this object to show up inside a webFlow context. + */ + private void writeObject(ObjectOutputStream out) throws IOException { + def map = [ + id : id, + dateCreated : dateCreated, + lastUpdated : lastUpdated, + + actor : actor, + uri : uri, + className : className, + persistedObjectId : persistedObjectId, + persistedObjectVersion: persistedObjectVersion, + + eventName : eventName, + propertyName : propertyName, + oldValue : oldValue, + newValue : newValue, + ] + out.writeObject(map) + } +} diff --git a/plugin/src/test/groovy/grails/plugins/orm/auditable/domain/Person.groovy b/plugin/src/test/groovy/grails/plugins/orm/auditable/domain/Person.groovy new file mode 100644 index 00000000..7a7176eb --- /dev/null +++ b/plugin/src/test/groovy/grails/plugins/orm/auditable/domain/Person.groovy @@ -0,0 +1,10 @@ +package grails.plugins.orm.auditable.domain + +import grails.gorm.annotation.Entity +import grails.plugins.orm.auditable.StampActor +import grails.plugins.orm.auditable.StampAutoTimestamp + +@Entity +class Person implements StampActor, StampAutoTimestamp { + String name +} \ No newline at end of file