#349 - ENH: Add @WhoCreated @WhoModified ... with CurrentUserProvider interface

This commit is contained in:
Robin Bygrave
2015-07-24 12:33:04 +12:00
parent a1ff9034b6
commit eae06059c9
21 changed files with 495 additions and 93 deletions
@@ -0,0 +1,29 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Mapped onto a entity bean property that represents the user id of
* who created the entity>
* <p>
* To use this annotation you need to implement CurrentUserProvider.
* The type of the bean property should match the type returned by
* CurrentUserProvider.
*</p>
* <h3>Example:</h3>
* <pre>{@code
*
* @WhoCreated
* String whoCreated;
*
* }</pre>
*/
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface WhoCreated {
}
@@ -0,0 +1,29 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Mapped onto a entity bean property that represents the user id of
* who last modified the entity>
* <p>
* To use this annotation you need to implement CurrentUserProvider.
* The type of the bean property should match the type returned by
* CurrentUserProvider.
* </p>
*
* <h3>Example:</h3>
* <pre>{@code
*
* @WhoModified
* String whoModified;
*
* }</pre>
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface WhoModified {
}
@@ -0,0 +1,16 @@
package com.avaje.ebean.config;
/**
* Provides the current user in order to support 'Who created', 'Who modified' and other audit features.
*/
public interface CurrentUserProvider {
/**
* Return the current user id.
* <p>
* The type returned should match the type of the properties annotated
* with @WhoCreated and @WhoModified. These are typically String, Long or UUID.
* </p>
*/
Object currentUser();
}
@@ -113,6 +113,12 @@ public class ServerConfig {
*/
private String classPathReaderClassName;
/**
* This is used to populate @WhoCreated, @WhoModified and
* support other audit features (who executed a query etc).
*/
private CurrentUserProvider currentUserProvider;
/**
* Config controlling the autofetch behaviour.
*/
@@ -446,6 +452,22 @@ public class ServerConfig {
this.defaultServer = defaultServer;
}
/**
* Return the CurrentUserProvider. This is used to populate @WhoCreated, @WhoModified and
* support other audit features (who executed a query etc).
*/
public CurrentUserProvider getCurrentUserProvider() {
return currentUserProvider;
}
/**
* Set the CurrentUserProvider. This is used to populate @WhoCreated, @WhoModified and
* support other audit features (who executed a query etc).
*/
public void setCurrentUserProvider(CurrentUserProvider currentUserProvider) {
this.currentUserProvider = currentUserProvider;
}
/**
* Return the PersistBatch mode to use by default at the transaction level.
* <p>
@@ -1812,6 +1834,7 @@ public class ServerConfig {
autoCommitMode = p.getBoolean("autoCommitMode", autoCommitMode);
useJtaTransactionManager = p.getBoolean("useJtaTransactionManager", useJtaTransactionManager);
currentUserProvider = createInstance(p, CurrentUserProvider.class, "currentUserProvider");
disableClasspathSearch = p.getBoolean("disableClasspathSearch", disableClasspathSearch);
databasePlatform = createInstance(p, DatabasePlatform.class, "databasePlatform");
encryptKeyManager = createInstance(p, EncryptKeyManager.class, "encryptKeyManager");
@@ -47,6 +47,13 @@ public class ClassUtil {
return classLoader;
}
/**
* Return true if javax validation annotations like Size and NotNull are present.
*/
public static boolean isJavaxValidationAnnotationsPresent() {
return isPresent("javax.validation.constraints.NotNull", null);
}
/**
* Return true if Jackson annotations like JsonIgnore are present.
*/
@@ -2,6 +2,7 @@ package com.avaje.ebeaninternal.server.core;
import javax.sql.DataSource;
import com.avaje.ebeaninternal.server.deploy.generatedproperty.GeneratedPropertyFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -92,7 +93,7 @@ public class InternalConfiguration {
private final XmlConfig xmlConfig;
private final JsonFactory jsonFactory;
public InternalConfiguration(XmlConfig xmlConfig, ClusterManager clusterManager,
ServerCacheManager cacheManager, SpiBackgroundExecutor backgroundExecutor,
ServerConfig serverConfig, BootupClasses bootupClasses, PstmtBatch pstmtBatch) {
@@ -267,4 +268,7 @@ public class InternalConfiguration {
return backgroundExecutor;
}
public GeneratedPropertyFactory getGeneratedPropertyFactory() {
return new GeneratedPropertyFactory(serverConfig.getCurrentUserProvider());
}
}
@@ -154,7 +154,7 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
this.eagerFetchLobs = serverConfig.isEagerFetchLobs();
this.asOfViewSuffix = serverConfig.getAsOfViewSuffix();
this.readAnnotations = new ReadAnnotations(asOfViewSuffix);
this.readAnnotations = new ReadAnnotations(config.getGeneratedPropertyFactory(), asOfViewSuffix);
this.bootupClasses = config.getBootupClasses();
this.createProperties = config.getDeployCreateProperties();
this.typeManager = config.getTypeManager();
@@ -3,6 +3,7 @@ package com.avaje.ebeaninternal.server.deploy.generatedproperty;
import java.math.BigDecimal;
import java.util.HashSet;
import com.avaje.ebean.config.CurrentUserProvider;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty;
/**
@@ -10,19 +11,27 @@ import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty;
*/
public class GeneratedPropertyFactory {
CounterFactory counterFactory;
private final CounterFactory counterFactory = new CounterFactory();;
InsertTimestampFactory insertFactory;
private final InsertTimestampFactory insertFactory = new InsertTimestampFactory();
UpdateTimestampFactory updateFactory;
private final UpdateTimestampFactory updateFactory = new UpdateTimestampFactory();
HashSet<String> numberTypes = new HashSet<String>();
private final HashSet<String> numberTypes = new HashSet<String>();
public GeneratedPropertyFactory() {
counterFactory = new CounterFactory();
insertFactory = new InsertTimestampFactory();
updateFactory = new UpdateTimestampFactory();
private final GeneratedWhoModified generatedWhoModified;
private final GeneratedWhoCreated generatedWhoCreated;
public GeneratedPropertyFactory(CurrentUserProvider currentUserProvider) {
if (currentUserProvider != null) {
generatedWhoCreated = new GeneratedWhoCreated(currentUserProvider);
generatedWhoModified = new GeneratedWhoModified(currentUserProvider);
} else {
generatedWhoCreated = null;
generatedWhoModified = null;
}
numberTypes.add(Integer.class.getName());
numberTypes.add(int.class.getName());
@@ -62,4 +71,18 @@ public class GeneratedPropertyFactory {
updateFactory.setUpdateTimestamp(property);
}
public void setWhoCreated(DeployBeanProperty property) {
if (generatedWhoCreated == null) {
throw new IllegalStateException("No CurrentUserProvider has been set so @WhoCreated is not supported");
}
property.setGeneratedProperty(generatedWhoCreated);
}
public void setWhoModified(DeployBeanProperty property) {
if (generatedWhoModified == null) {
throw new IllegalStateException("No CurrentUserProvider has been set so @WhoModified is not supported");
}
property.setGeneratedProperty(generatedWhoModified);
}
}
@@ -0,0 +1,47 @@
package com.avaje.ebeaninternal.server.deploy.generatedproperty;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebean.config.CurrentUserProvider;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
/**
* Used to populate @WhoCreated bean properties.
*/
public class GeneratedWhoCreated implements GeneratedProperty {
final CurrentUserProvider currentUserProvider;
public GeneratedWhoCreated(CurrentUserProvider currentUserProvider) {
this.currentUserProvider = currentUserProvider;
}
@Override
public Object getInsertValue(BeanProperty prop, EntityBean bean) {
return currentUserProvider.currentUser();
}
@Override
public Object getUpdateValue(BeanProperty prop, EntityBean bean) {
return null;
}
@Override
public boolean includeInUpdate() {
return false;
}
@Override
public boolean includeInAllUpdates() {
return false;
}
@Override
public boolean includeInInsert() {
return true;
}
@Override
public boolean isDDLNotNullable() {
return true;
}
}
@@ -0,0 +1,47 @@
package com.avaje.ebeaninternal.server.deploy.generatedproperty;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebean.config.CurrentUserProvider;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
/**
* Used to populate @WhoModified bean properties.
*/
public class GeneratedWhoModified implements GeneratedProperty {
final CurrentUserProvider currentUserProvider;
public GeneratedWhoModified(CurrentUserProvider currentUserProvider) {
this.currentUserProvider = currentUserProvider;
}
@Override
public Object getInsertValue(BeanProperty prop, EntityBean bean) {
return currentUserProvider.currentUser();
}
@Override
public Object getUpdateValue(BeanProperty prop, EntityBean bean) {
return currentUserProvider.currentUser();
}
@Override
public boolean includeInUpdate() {
return true;
}
@Override
public boolean includeInAllUpdates() {
return true;
}
@Override
public boolean includeInInsert() {
return true;
}
@Override
public boolean isDDLNotNullable() {
return true;
}
}
@@ -34,8 +34,8 @@ public class AnnotationAssocManys extends AnnotationParser {
/**
* Create with the DeployInfo.
*/
public AnnotationAssocManys(DeployBeanInfo<?> info, BeanDescriptorManager factory) {
super(info);
public AnnotationAssocManys(DeployBeanInfo<?> info, boolean javaxValidationAnnotations, BeanDescriptorManager factory) {
super(info, javaxValidationAnnotations);
this.factory = factory;
}
@@ -33,8 +33,8 @@ public class AnnotationAssocOnes extends AnnotationParser {
/**
* Create with the deploy Info.
*/
public AnnotationAssocOnes(DeployBeanInfo<?> info, BeanDescriptorManager factory) {
super(info);
public AnnotationAssocOnes(DeployBeanInfo<?> info, boolean javaxValidationAnnotations, BeanDescriptorManager factory) {
super(info, javaxValidationAnnotations);
this.factory = factory;
}
@@ -29,8 +29,8 @@ public class AnnotationClass extends AnnotationParser {
private final String asOfViewSuffix;
public AnnotationClass(DeployBeanInfo<?> info, String asOfViewSuffix) {
super(info);
public AnnotationClass(DeployBeanInfo<?> info, boolean validationAnnotations, String asOfViewSuffix) {
super(info, validationAnnotations);
this.asOfViewSuffix = asOfViewSuffix;
}
@@ -1,25 +1,11 @@
package com.avaje.ebeaninternal.server.deploy.parse;
import com.avaje.ebean.annotation.CreatedTimestamp;
import com.avaje.ebean.annotation.DbHstore;
import com.avaje.ebean.annotation.DbJson;
import com.avaje.ebean.annotation.DbJsonB;
import com.avaje.ebean.annotation.EmbeddedColumns;
import com.avaje.ebean.annotation.Encrypted;
import com.avaje.ebean.annotation.Expose;
import com.avaje.ebean.annotation.Formula;
import com.avaje.ebean.annotation.HistoryExclude;
import com.avaje.ebean.annotation.Index;
import com.avaje.ebean.annotation.JsonIgnore;
import com.avaje.ebean.annotation.UpdatedTimestamp;
import com.avaje.ebean.annotation.WhenCreated;
import com.avaje.ebean.annotation.WhenModified;
import com.avaje.ebean.annotation.*;
import com.avaje.ebean.config.EncryptDeploy;
import com.avaje.ebean.config.EncryptDeploy.Mode;
import com.avaje.ebean.config.dbplatform.DbEncrypt;
import com.avaje.ebean.config.dbplatform.DbEncryptFunction;
import com.avaje.ebean.config.dbplatform.IdType;
import com.avaje.ebeaninternal.api.ClassUtil;
import com.avaje.ebeaninternal.server.deploy.generatedproperty.GeneratedPropertyFactory;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssoc;
@@ -49,17 +35,22 @@ public class AnnotationFields extends AnnotationParser {
/**
* If present read Jackson JsonIgnore.
*/
private boolean jacksonAnnotationsPresent = ClassUtil.isJacksonAnnotationsPresent();
private final boolean jacksonAnnotationsPresent;
private final GeneratedPropertyFactory generatedPropFactory;
/**
* By default we lazy load Lob properties.
*/
private FetchType defaultLobFetchType = FetchType.LAZY;
private GeneratedPropertyFactory generatedPropFactory = new GeneratedPropertyFactory();
public AnnotationFields(GeneratedPropertyFactory generatedPropFactory, DeployBeanInfo<?> info,
boolean javaxValidationAnnotations, boolean jacksonAnnotationsPresent, boolean eagerFetchLobs) {
public AnnotationFields(DeployBeanInfo<?> info, boolean eagerFetchLobs) {
super(info);
super(info, javaxValidationAnnotations);
this.jacksonAnnotationsPresent = jacksonAnnotationsPresent;
this.generatedPropFactory = generatedPropFactory;
if (eagerFetchLobs) {
defaultLobFetchType = FetchType.EAGER;
@@ -218,6 +209,13 @@ public class AnnotationFields extends AnnotationParser {
generatedPropFactory.setUpdateTimestamp(prop);
}
if (get(prop, WhoCreated.class) != null) {
generatedPropFactory.setWhoCreated(prop);
}
if (get(prop, WhoModified.class) != null) {
generatedPropFactory.setWhoModified(prop);
}
if (get(prop, HistoryExclude.class) != null) {
prop.setExcludedFromHistory(true);
}
@@ -21,22 +21,14 @@ public abstract class AnnotationParser extends AnnotationBase {
protected final Class<?> beanType;
protected boolean validationAnnotations;
protected final boolean validationAnnotations;
public AnnotationParser(DeployBeanInfo<?> info) {
public AnnotationParser(DeployBeanInfo<?> info, boolean validationAnnotations) {
super(info.getUtil());
this.validationAnnotations = validationAnnotations;
this.info = info;
this.beanType = info.getDescriptor().getBeanType();
this.descriptor = info.getDescriptor();
try {
Class.forName("javax.validation.constraints.NotNull");
validationAnnotations = true;
} catch (ClassNotFoundException e) {
// javax.validation not in the classpath so don't
// check for NotNull and Size
validationAnnotations = false;
}
}
/**
@@ -9,8 +9,8 @@ import com.avaje.ebeaninternal.server.deploy.DRawSqlMeta;
*/
public class AnnotationSql extends AnnotationParser {
public AnnotationSql(DeployBeanInfo<?> info) {
super(info);
public AnnotationSql(DeployBeanInfo<?> info, boolean javaxValidationAnnotations) {
super(info, javaxValidationAnnotations);
}
public void parse() {
@@ -1,6 +1,8 @@
package com.avaje.ebeaninternal.server.deploy.parse;
import com.avaje.ebeaninternal.api.ClassUtil;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptorManager;
import com.avaje.ebeaninternal.server.deploy.generatedproperty.GeneratedPropertyFactory;
/**
@@ -8,61 +10,76 @@ import com.avaje.ebeaninternal.server.deploy.BeanDescriptorManager;
*/
public class ReadAnnotations {
/**
* Creates appropriate generated properties - WhenXXX, WhoXXX, Version etc.
*/
private final GeneratedPropertyFactory generatedPropFactory;
/**
* Typically _with_history and when appended to the base table derives the name of
* the view that unions the base table with the history table to support asOf queries.
*/
private final String asOfViewSuffix;
public ReadAnnotations(String asOfViewSuffix) {
/**
* True if the javax validation annotations are present in the classpath.
*/
private final boolean javaxValidationAnnotations;
/**
* True if the jackson annotations are present in the classpath.
*/
private final boolean jacksonAnnotations;
public ReadAnnotations(GeneratedPropertyFactory generatedPropFactory, String asOfViewSuffix) {
this.generatedPropFactory = generatedPropFactory;
this.asOfViewSuffix = asOfViewSuffix;
this.javaxValidationAnnotations = ClassUtil.isJavaxValidationAnnotationsPresent();
this.jacksonAnnotations = ClassUtil.isJacksonAnnotationsPresent();
}
/**
* Read the initial non-relationship annotations included Id and EmbeddedId.
* <p>
* We then have enough to create BeanTables which are used in readAssociations
* to resolve the relationships etc.
* </p>
*/
public void readInitial(DeployBeanInfo<?> info, boolean eagerFetchLobs){
* Read the initial non-relationship annotations included Id and EmbeddedId.
* <p>
* We then have enough to create BeanTables which are used in readAssociations
* to resolve the relationships etc.
* </p>
*/
public void readInitial(DeployBeanInfo<?> info, boolean eagerFetchLobs) {
try {
new AnnotationClass(info, asOfViewSuffix).parse();
new AnnotationFields(info, eagerFetchLobs).parse();
} catch (RuntimeException e){
String msg = "Error reading annotations for "+info;
throw new RuntimeException(msg, e);
}
}
try {
new AnnotationClass(info, javaxValidationAnnotations, asOfViewSuffix).parse();
new AnnotationFields(generatedPropFactory, info, javaxValidationAnnotations, jacksonAnnotations, eagerFetchLobs).parse();
/**
* Read and process the associated relationship annotations.
* <p>
* These can only be processed after the BeanTables have been created
* </p>
* <p>
* This uses the factory as a call back to get the BeanTable for a given
* associated bean.
* </p>
*/
public void readAssociations(DeployBeanInfo<?> info, BeanDescriptorManager factory){
try {
new AnnotationAssocOnes(info, factory).parse();
new AnnotationAssocManys(info, factory).parse();
// read the Sql annotations last because they may be
// dependent on field level annotations
new AnnotationSql(info).parse();
} catch (RuntimeException e){
String msg = "Error reading annotations for "+info;
throw new RuntimeException(msg, e);
}
} catch (RuntimeException e) {
String msg = "Error reading annotations for " + info;
throw new RuntimeException(msg, e);
}
}
/**
* Read and process the associated relationship annotations.
* <p>
* These can only be processed after the BeanTables have been created
* </p>
* <p>
* This uses the factory as a call back to get the BeanTable for a given
* associated bean.
* </p>
*/
public void readAssociations(DeployBeanInfo<?> info, BeanDescriptorManager factory) {
try {
new AnnotationAssocOnes(info, javaxValidationAnnotations, factory).parse();
new AnnotationAssocManys(info, javaxValidationAnnotations, factory).parse();
// read the Sql annotations last because they may be
// dependent on field level annotations
new AnnotationSql(info, javaxValidationAnnotations).parse();
} catch (RuntimeException e) {
throw new RuntimeException("Error reading annotations for " + info, e);
}
}
}
@@ -0,0 +1,32 @@
package com.avaje.ebean;
import com.avaje.ebean.config.CurrentUserProvider;
/**
* Returns the current user typically from a Thread local or similar context.
*/
public class MyCurrentUserProvider implements CurrentUserProvider {
public static final String DEFAULT = "42789";
/**
* Do not do this yourself - this is for testing purposes.
*/
private static String userId = DEFAULT;
@Override
public Object currentUser() {
// just hardcoding here for testing
return userId;
}
public static void setUserId(String value) {
userId = value;
}
public static void resetToDefault() {
userId = DEFAULT;
}
}
@@ -0,0 +1,46 @@
package com.avaje.tests.generated;
import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.ebean.MyCurrentUserProvider;
import com.avaje.tests.model.EWhoProps;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class TestGeneratedWho extends BaseTestCase {
@Test
public void insertUpdate() {
EWhoProps bean = new EWhoProps();
bean.setName("one");
MyCurrentUserProvider.setUserId("INSERT_WHO_1");
Ebean.save(bean);
assertEquals("one", bean.getName());
assertEquals("INSERT_WHO_1", bean.getWhoCreated());
assertEquals("INSERT_WHO_1", bean.getWhoModified());
assertNotNull(bean.getWhenCreated());
assertNotNull(bean.getWhenModified());
MyCurrentUserProvider.setUserId("UPDATE_WHO_1");
bean.setName("two");
Ebean.save(bean);
assertEquals("two", bean.getName());
assertEquals("INSERT_WHO_1", bean.getWhoCreated());
assertEquals("UPDATE_WHO_1", bean.getWhoModified());
MyCurrentUserProvider.setUserId("UPDATE_WHO_2");
bean.setName("three");
Ebean.save(bean);
assertEquals("INSERT_WHO_1", bean.getWhoCreated());
assertEquals("UPDATE_WHO_2", bean.getWhoModified());
MyCurrentUserProvider.resetToDefault();
}
}
@@ -0,0 +1,91 @@
package com.avaje.tests.model;
import com.avaje.ebean.annotation.WhenCreated;
import com.avaje.ebean.annotation.WhenModified;
import com.avaje.ebean.annotation.WhoCreated;
import com.avaje.ebean.annotation.WhoModified;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Version;
import java.sql.Timestamp;
@Entity
public class EWhoProps {
@Id
Long id;
String name;
@Version
Long version;
@WhenCreated
Timestamp whenCreated;
@WhenModified
Timestamp whenModified;
@WhoCreated
String whoCreated;
@WhoModified
String whoModified;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
public Timestamp getWhenCreated() {
return whenCreated;
}
public void setWhenCreated(Timestamp whenCreated) {
this.whenCreated = whenCreated;
}
public Timestamp getWhenModified() {
return whenModified;
}
public void setWhenModified(Timestamp whenModified) {
this.whenModified = whenModified;
}
public String getWhoCreated() {
return whoCreated;
}
public void setWhoCreated(String whoCreated) {
this.whoCreated = whoCreated;
}
public String getWhoModified() {
return whoModified;
}
public void setWhoModified(String whoModified) {
this.whoModified = whoModified;
}
}
+1
View File
@@ -27,6 +27,7 @@ ebean.ddl.run=true
ebean.persistBatch=NONE
ebean.persistBatchOnCascade=ALL
ebean.currentUserProvider=com.avaje.ebean.MyCurrentUserProvider
#ebean.jsonInclude=NON_NULL