#390 - ENH: Add Change log mechanism for easy fine grained logging of insert, update, delete activity

This commit is contained in:
Robin Bygrave
2015-08-20 20:46:28 +12:00
parent 46b158a0a0
commit 84190f3c2a
34 changed files with 1642 additions and 84 deletions
@@ -0,0 +1,21 @@
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;
/**
* Marks an entity bean as being included in the change logging.
*/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface ChangeLog {
/**
* Set this to true to exclude inserts on the associated bean type
* from being included in the change log.
*/
boolean excludeInserts() default false;
}
@@ -8,6 +8,9 @@ import com.avaje.ebean.cache.ServerCacheManager;
import com.avaje.ebean.config.dbplatform.DatabasePlatform;
import com.avaje.ebean.config.dbplatform.DbEncrypt;
import com.avaje.ebean.event.*;
import com.avaje.ebean.event.changelog.ChangeLogListener;
import com.avaje.ebean.event.changelog.ChangeLogPrepare;
import com.avaje.ebean.event.changelog.ChangeLogRegister;
import com.avaje.ebean.meta.MetaInfoManager;
import com.avaje.ebean.util.ClassUtil;
import com.fasterxml.jackson.core.JsonFactory;
@@ -280,6 +283,12 @@ public class ServerConfig {
private List<ServerConfigStartup> configStartupListeners = new ArrayList<ServerConfigStartup>();
private List<TransactionEventListener> transactionEventListeners = new ArrayList<TransactionEventListener>();
private ChangeLogPrepare changeLogPrepare;
private ChangeLogListener changeLogListener;
private ChangeLogRegister changeLogRegister;
private EncryptKeyManager encryptKeyManager;
private EncryptDeployManager encryptDeployManager;
@@ -636,6 +645,62 @@ public class ServerConfig {
this.databaseSequenceBatchSize = databaseSequenceBatchSize;
}
/**
* Return the ChangeLogPrepare.
* <p>
* This is used to set user context information to the ChangeSet in the
* foreground thread prior to the logging occurring in a background thread.
* </p>
*/
public ChangeLogPrepare getChangeLogPrepare() {
return changeLogPrepare;
}
/**
* Set the ChangeLogPrepare.
* <p>
* This is used to set user context information to the ChangeSet in the
* foreground thread prior to the logging occurring in a background thread.
* </p>
*/
public void setChangeLogPrepare(ChangeLogPrepare changeLogPrepare) {
this.changeLogPrepare = changeLogPrepare;
}
/**
* Return the ChangeLogListener which actually performs the logging of change sets
* in the background.
*/
public ChangeLogListener getChangeLogListener() {
return changeLogListener;
}
/**
* Set the ChangeLogListener which actually performs the logging of change sets
* in the background.
*/
public void setChangeLogListener(ChangeLogListener changeLogListener) {
this.changeLogListener = changeLogListener;
}
/**
* Return the ChangeLogRegister which controls which ChangeLogFilter is used for each
* bean type and in this way provide fine grained control over which persist requests
* are included in the change log.
*/
public ChangeLogRegister getChangeLogRegister() {
return changeLogRegister;
}
/**
* Set the ChangeLogRegister which controls which ChangeLogFilter is used for each
* bean type and in this way provide fine grained control over which persist requests
* are included in the change log.
*/
public void setChangeLogRegister(ChangeLogRegister changeLogRegister) {
this.changeLogRegister = changeLogRegister;
}
/**
* Return the DB migration configuration.
*/
@@ -0,0 +1,123 @@
package com.avaje.ebean.event.changelog;
import com.avaje.ebean.ValuePair;
import java.util.Map;
/**
* A bean insert, update or delete change sent as part of a ChangeSet.
*/
public class BeanChange {
/**
* The underling base table name.
*/
String table;
/**
* The id value.
*/
Object id;
/**
* The INSERT, UPDATE or DELETE change type.
*/
ChangeType type;
/**
* The time the bean change was created.
*/
long eventTime;
/**
* The values for insert or update. Note that null values are not included for insert.
*/
Map<String, ValuePair> values;
/**
* Construct with all the details.
*/
public BeanChange(String table, Object id, ChangeType type, Map<String, ValuePair> values) {
this.table = table;
this.id = id;
this.type = type;
this.eventTime = System.currentTimeMillis();
this.values = values;
}
/**
* Default constructor for JSON tools.
*/
public BeanChange() {
}
/**
* Return the object type (typically table name).
*/
public String getTable() {
return table;
}
/**
* Set the object type (for JSON tools).
*/
public void setTable(String table) {
this.table = table;
}
/**
* Return the object id.
*/
public Object getId() {
return id;
}
/**
* Set the bean id (for JSON tools).
*/
public void setId(Object id) {
this.id = this.id;
}
/**
* Return the change type (INSERT, UPDATE or DELETE).
*/
public ChangeType getType() {
return type;
}
/**
* Set the type (for JSON tools).
*/
public void setType(ChangeType type) {
this.type = type;
}
/**
* Return the event time in epoch millis.
*/
public long getEventTime() {
return eventTime;
}
/**
* Set the event time in epoch millis.
*/
public void setEventTime(long eventTime) {
this.eventTime = eventTime;
}
/**
* Return the value pairs. For inserts the ValuePair oldValue is always null.
*/
public Map<String, ValuePair> getValues() {
return values;
}
/**
* Set the value pairs (for JSON tools).
*/
public void setValues(Map<String, ValuePair> values) {
this.values = values;
}
}
@@ -0,0 +1,25 @@
package com.avaje.ebean.event.changelog;
import com.avaje.ebean.event.BeanPersistRequest;
/**
* Used to provide fine grained control over what persist requests are included in the change log.
*/
public interface ChangeLogFilter {
/**
* Return true if this insert request should be included in the change log.
*/
boolean includeInsert(BeanPersistRequest<?> insertRequest);
/**
* Return true if this update request should be included in the change log.
*/
boolean includeUpdate(BeanPersistRequest<?> updateRequest);
/**
* Return true if this delete request should be included in the change log.
*/
boolean includeDelete(BeanPersistRequest<?> deleteRequest);
}
@@ -0,0 +1,21 @@
package com.avaje.ebean.event.changelog;
/**
* Listen for changes.
* <p>
* Implementations can take the changes and store them in a document store for auditing purposes etc.
* </p>
*/
public interface ChangeLogListener {
/**
* Log the batch of changes.
* <p>
* For small transactions this will be all the changes in the transaction.
* For larger/longer transactions this can be a 'batch' of changes made and the actual transaction
* has not yet committed or rolled back and a later change set will contain the final changeSet for
* the transaction with it's final status of <code>COMMITTED</code> or <code>ROLLBACK</code>.
*/
void log(ChangeSet changeSet);
}
@@ -0,0 +1,27 @@
package com.avaje.ebean.event.changelog;
/**
* Listen for changes.
* <p>
* Implementations can take the changes and store them in a document store for auditing purposes etc.
* </p>
*/
public interface ChangeLogPrepare {
/**
* In the foreground prepare the changeLog for sending.
* <p>
* This is intended to set extra context information onto the ChangeSet such
* as the application user id and client ip address.
* </p>
* <p>
* Returning false means the changeLog is not sent to the log() method in a background thread
* and implies that the changeSet should be ignored or that is has been handled in this prepare()
* method call.
* </p>
*
* @return true if the changeLog should then be sent to the log method in a background thread.
*/
boolean prepare(ChangeSet changeSet);
}
@@ -0,0 +1,18 @@
package com.avaje.ebean.event.changelog;
/**
* Used to assign ChangeLogFilters to bean types.
* <p>
* Ebean has a built in implementation that uses the ChangeLog annotation to build
* appropriate ChangeLogFilters but you can provide an implementation to use instead.
* </p>
*/
public interface ChangeLogRegister {
/**
* For the given bean type return the Change log filter to use.
* <p>
* This filter provides control over which persist request are included in the change log.
*/
ChangeLogFilter getChangeFilter(Class<?> beanType);
}
@@ -0,0 +1,202 @@
package com.avaje.ebean.event.changelog;
import java.util.ArrayList;
import java.util.List;
/**
* Holds a set of changes.
*/
public class ChangeSet {
/**
* A UUID transaction id specifically created for the change set.
*/
String txnId;
/**
* For large transactions with many change sets this is an incrementing counter.
*/
long txnBatch;
/**
* The state of the transaction (change sets can be sent prior to commit or rollback
* with large transactions).
*/
TxnState txnState;
/**
* User defined 'source' such as the application name.
*/
String source;
/**
* Application user id expected to be optionally populated by ChangeLogPrepare.
*/
String userId;
/**
* Application user ip address expected to be optionally populated by ChangeLogPrepare.
*/
String userIpAddress;
/**
* Arbitrary user context information expected to be optionally populated by ChangeLogPrepare.
*/
String userContext;
/**
* The bean changes.
*/
List<BeanChange> changes = new ArrayList<BeanChange>();
/**
* Construct with a txnId.
*/
public ChangeSet(String txnId, long txnBatch) {
this.txnId = txnId;
this.txnBatch = txnBatch;
this.txnState = TxnState.IN_PROGRESS;
}
/**
* Default constructor for JSON tools.
*/
public ChangeSet() {
}
public String toString() {
return "txnId:" + txnId + " txnState:" + txnState + " txnBatch:" + txnBatch;
}
/**
* Add a bean change to the change set.
*/
public void addBeanChange(BeanChange beanChange) {
changes.add(beanChange);
}
/**
* Return the txnId.
*/
public String getTxnId() {
return txnId;
}
/**
* Set the txnId (used by JSON tools).
*/
public void setTxnId(String txnId) {
this.txnId = txnId;
}
/**
* Returns the batch id.
*/
public long getTxnBatch() {
return txnBatch;
}
/**
* Sets the batch id (used by JSON tools).
*/
public void setTxnBatch(long txnBatch) {
this.txnBatch = txnBatch;
}
/**
* Return the transaction state. This will be IN_PROGRESS for many changeSets in large transactions
* as the changeSets are sent in batches before the transaction has completed.
*/
public TxnState getTxnState() {
return txnState;
}
/**
* Set the state (used by JSON tools).
*/
public void setTxnState(TxnState txnState) {
this.txnState = txnState;
}
/**
* Return a code that identifies the source of the change (like the name of the application).
*/
public String getSource() {
return source;
}
/**
* Set the source of the change (like the name of the application).
*/
public void setSource(String source) {
this.source = source;
}
/**
* Return the application user Id.
*/
public String getUserId() {
return userId;
}
/**
* Set the application user Id.
* <p>
* This can be set by the ChangeLogListener in the prepare() method which is called
* in the foreground thread.
* </p>
*/
public void setUserId(String userId) {
this.userId = userId;
}
/**
* Return the application users ip address.
*/
public String getUserIpAddress() {
return userIpAddress;
}
/**
* Set the application users ip address.
* <p>
* This can be set by the ChangeLogListener in the prepare() method which is called
* in the foreground thread.
* </p>
*/
public void setUserIpAddress(String userIpAddress) {
this.userIpAddress = userIpAddress;
}
/**
* Return a user context value - anything you set yourself in ChangeLogListener prepare().
*/
public String getUserContext() {
return userContext;
}
/**
* Set a user context value (anything you like).
* <p>
* This can be set by the ChangeLogListener in the prepare() method which is called
* in the foreground thread.
* </p>
*/
public void setUserContext(String userContext) {
this.userContext = userContext;
}
/**
* Return the bean changes.
*/
public List<BeanChange> getChanges() {
return changes;
}
/**
* Set the bean changes (used by JSON tools).
*/
public void setChanges(List<BeanChange> changes) {
this.changes = changes;
}
}
@@ -0,0 +1,32 @@
package com.avaje.ebean.event.changelog;
/**
* The type of the change.
*/
public enum ChangeType {
/**
* The change was an insert.
*/
INSERT("I"),
/**
* The change was an update.
*/
UPDATE("U"),
/**
* The change was a delete.
*/
DELETE("D");
final String code;
ChangeType(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
@@ -0,0 +1,40 @@
package com.avaje.ebean.event.changelog;
/**
* Transaction state when ChangeSets are sent to the ChangeSetListener.
* <p>
* For large transactions multiple ChangeSets can be sent in a batch fashion and in this
* case all but the last changeSet with have IN_PROGRESS state and the last changeSet will
* have the COMMITTED or ROLLBACK state.
* </p>
*/
public enum TxnState {
/**
* The Transaction is still in progress.
* <p>
* Used when the transaction is large/long and Ebean wants to send out the changeSets
* in batches and a changeSet is send before the transaction has completed.
*/
IN_PROGRESS("I"),
/**
* The Transaction was committed.
*/
COMMITTED("C"),
/**
* The Transaction was rolled back.
*/
ROLLBACK("R");
private final String code;
TxnState(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
@@ -201,4 +201,15 @@ public interface JsonContext {
* @throws JsonIOException When IOException occurs
*/
JsonParser createParser(Reader reader) throws JsonIOException;
/**
* Return a helper that can write scalar types known to Ebean to Jackson.
* <p>
* Ebean has built in support for java8 and Joda types as well as the other
* standard JDK types like URI, URL, UUID etc. This is a fast simple way to
* write any of those types to Jackson.
* </p>
*/
JsonScalar getScalar(JsonGenerator generator);
}
@@ -0,0 +1,11 @@
package com.avaje.ebean.text.json;
import java.io.IOException;
/**
* Writes any scalar type known to Ebean the Jackson generator.
*/
public interface JsonScalar {
void write(String name, Object value) throws IOException;
}
@@ -3,6 +3,8 @@ package com.avaje.ebeaninternal.api;
import com.avaje.ebean.TransactionCallback;
import com.avaje.ebean.bean.PersistenceContext;
import com.avaje.ebean.config.PersistBatch;
import com.avaje.ebean.event.changelog.BeanChange;
import com.avaje.ebean.event.changelog.ChangeSet;
import com.avaje.ebeaninternal.server.core.PersistRequest;
import com.avaje.ebeaninternal.server.core.PersistRequestBean;
import com.avaje.ebeaninternal.server.persist.BatchControl;
@@ -87,6 +89,16 @@ public class ScopedTransaction implements SpiTransaction {
transaction.logSummary(msg);
}
@Override
public void addBeanChange(BeanChange beanChange) {
transaction.addBeanChange(beanChange);
}
@Override
public void sendChangeLog(ChangeSet changes) {
transaction.sendChangeLog(changes);
}
@Override
public void registerDerivedRelationship(DerivedRelationshipData assocBean) {
transaction.registerDerivedRelationship(assocBean);
@@ -25,7 +25,7 @@ import java.util.List;
* Service Provider extension to EbeanServer.
*/
public interface SpiEbeanServer extends EbeanServer, BeanLoader, BeanCollectionLoader {
/**
* For internal use, shutdown of the server invoked by JVM Shutdown.
*/
@@ -5,6 +5,8 @@ import java.util.List;
import com.avaje.ebean.Transaction;
import com.avaje.ebean.bean.PersistenceContext;
import com.avaje.ebean.event.changelog.BeanChange;
import com.avaje.ebean.event.changelog.ChangeSet;
import com.avaje.ebeaninternal.server.core.PersistRequest;
import com.avaje.ebeaninternal.server.core.PersistRequestBean;
import com.avaje.ebeaninternal.server.persist.BatchControl;
@@ -233,4 +235,13 @@ public interface SpiTransaction extends Transaction {
*/
void flushBatchOnCollection();
/**
* Add a bean change to the change log.
*/
void addBeanChange(BeanChange beanChange);
/**
* Send the change set to be prepared and then logged.
*/
void sendChangeLog(ChangeSet changeSet);
}
@@ -1,23 +1,31 @@
package com.avaje.ebeaninternal.server.core;
import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.Table;
import com.avaje.ebean.event.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.avaje.ebean.config.CompoundType;
import com.avaje.ebean.config.ScalarTypeConverter;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.event.BeanFindController;
import com.avaje.ebean.event.BeanPersistController;
import com.avaje.ebean.event.BeanPersistListener;
import com.avaje.ebean.event.BeanPostLoad;
import com.avaje.ebean.event.BeanQueryAdapter;
import com.avaje.ebean.event.ServerConfigStartup;
import com.avaje.ebean.event.TransactionEventListener;
import com.avaje.ebean.event.changelog.ChangeLogListener;
import com.avaje.ebean.event.changelog.ChangeLogPrepare;
import com.avaje.ebean.event.changelog.ChangeLogRegister;
import com.avaje.ebeaninternal.server.type.ScalarType;
import com.avaje.ebeaninternal.server.util.ClassPathSearchMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
/**
* Interesting classes for a EbeanServer such as Embeddable, Entity,
@@ -58,6 +66,14 @@ public class BootupClasses implements ClassPathSearchMatcher {
private final List<BeanQueryAdapter> queryAdapterInstances = new ArrayList<BeanQueryAdapter>();
private final List<TransactionEventListener> transactionEventListenerInstances = new ArrayList<TransactionEventListener>();
private Class<?> changeLogPrepareClass;
private Class<?> changeLogListenerClass;
private Class<?> changeLogRegisterClass;
private ChangeLogPrepare changeLogPrepare;
private ChangeLogListener changeLogListener;
private ChangeLogRegister changeLogRegister;
public BootupClasses() {
}
@@ -175,98 +191,122 @@ public class BootupClasses implements ClassPathSearchMatcher {
}
}
public List<BeanQueryAdapter> getBeanQueryAdapters() {
// add class registered BeanQueryAdapter to the
// already created instances
for (Class<?> cls : beanQueryAdapterList) {
try {
BeanQueryAdapter newInstance = (BeanQueryAdapter) cls.newInstance();
queryAdapterInstances.add(newInstance);
} catch (Exception e) {
String msg = "Error creating BeanQueryAdapter " + cls;
logger.error(msg, e);
public void addChangeLogInstances(ServerConfig serverConfig) {
changeLogListener = serverConfig.getChangeLogListener();
changeLogRegister = serverConfig.getChangeLogRegister();
changeLogPrepare = serverConfig.getChangeLogPrepare();
// if not already set create the implementations found
// via classpath scanning
if (changeLogPrepare == null && changeLogPrepareClass != null) {
changeLogPrepare = (ChangeLogPrepare)create(changeLogPrepareClass, false);
}
if (changeLogListener == null && changeLogListenerClass != null) {
changeLogListener = (ChangeLogListener)create(changeLogListenerClass, false);
}
if (changeLogRegister == null && changeLogRegisterClass != null) {
changeLogRegister = (ChangeLogRegister)create(changeLogRegisterClass, false);
}
}
/**
* Create an instance using the default constructor returning null if there
* is no default constructor (implying the class was not intended to be instantiated
* automatically via classpath scanning.
* <p>
* Use logOnException = true to log the error and carry on.
*/
private Object create(Class<?> cls, boolean logOnException) {
try {
// instantiate via found class
Constructor constructor = cls.getConstructor();
return constructor.newInstance();
} catch (NoSuchMethodException e) {
logger.debug("Ignore/expected - no default constructor", e);
return null;
} catch (Exception e) {
if (logOnException) {
// not expected but we log and carry on
logger.error("Error creating " + cls, e);
return null;
} else {
// ok, stop the bus
throw new IllegalStateException("Error creating " + cls, e);
}
}
}
/**
* Create the instance if it has a default constructor and add it to the list of instances.
*/
@SuppressWarnings(value = "unchecked")
private <T> void createAdd(Class<?> cls, List<T> instances) {
Object newInstance = create(cls, true);
if (newInstance != null) {
instances.add((T)newInstance);
}
}
public ChangeLogPrepare getChangeLogPrepare() {
return changeLogPrepare;
}
public ChangeLogListener getChangeLogListener() {
return changeLogListener;
}
public ChangeLogRegister getChangeLogRegister() {
return changeLogRegister;
}
public List<BeanQueryAdapter> getBeanQueryAdapters() {
// add class registered BeanQueryAdapter to the already created instances
for (Class<?> cls : beanQueryAdapterList) {
createAdd(cls, queryAdapterInstances);
}
return queryAdapterInstances;
}
public List<BeanFindController> getBeanFindControllers() {
// add class registered BeanFindController to the
// list of created instances
// add class registered BeanFindController to the list of created instances
for (Class<?> cls : beanFindControllerList) {
try {
BeanFindController newInstance = (BeanFindController) cls.newInstance();
findControllerInstances.add(newInstance);
} catch (Exception e) {
String msg = "Error creating BeanPersistController " + cls;
logger.error(msg, e);
}
createAdd(cls, findControllerInstances);
}
return findControllerInstances;
}
public List<BeanPersistListener> getBeanPersistListeners() {
// add class registered BeanPersistController to the
// already created instances
// add class registered BeanPersistController to the already created instances
for (Class<?> cls : beanListenerList) {
try {
BeanPersistListener newInstance = (BeanPersistListener) cls.newInstance();
persistListenerInstances.add(newInstance);
} catch (Exception e) {
String msg = "Error creating BeanPersistController " + cls;
logger.error(msg, e);
}
createAdd(cls, persistListenerInstances);
}
return persistListenerInstances;
}
public List<BeanPersistController> getBeanPersistControllers() {
// add class registered BeanPersistController to the
// already created instances
// add class registered BeanPersistController to the already created instances
for (Class<?> cls : beanControllerList) {
try {
BeanPersistController newInstance = (BeanPersistController) cls.newInstance();
persistControllerInstances.add(newInstance);
} catch (Exception e) {
String msg = "Error creating BeanPersistController " + cls;
logger.error(msg, e);
}
createAdd(cls, persistControllerInstances);
}
return persistControllerInstances;
}
public List<BeanPostLoad> getBeanPostLoaders() {
// add class registered BeanPostLoad to the already created instances
for (Class<?> cls : beanPostLoadList) {
try {
BeanPostLoad newInstance = (BeanPostLoad) cls.newInstance();
beanPostLoadInstances.add(newInstance);
} catch (Exception e) {
String msg = "Error creating BeanPersistController " + cls;
logger.error(msg, e);
}
createAdd(cls, beanPostLoadInstances);
}
return beanPostLoadInstances;
}
public List<TransactionEventListener> getTransactionEventListeners() {
// add class registered TransactionEventListener to the
// already created instances
// add class registered TransactionEventListener to the already created instances
for (Class<?> cls : transactionEventListenerList) {
try {
TransactionEventListener newInstance = (TransactionEventListener) cls.newInstance();
transactionEventListenerInstances.add(newInstance);
} catch (Exception e) {
String msg = "Error creating TransactionEventListener " + cls;
logger.error(msg, e);
}
createAdd(cls, transactionEventListenerInstances);
}
return transactionEventListenerInstances;
}
@@ -385,6 +425,21 @@ public class BootupClasses implements ClassPathSearchMatcher {
interesting = true;
}
if (ChangeLogListener.class.isAssignableFrom(cls)) {
changeLogListenerClass = cls;
interesting = true;
}
if (ChangeLogRegister.class.isAssignableFrom(cls)) {
changeLogRegisterClass = cls;
interesting = true;
}
if (ChangeLogPrepare.class.isAssignableFrom(cls)) {
changeLogPrepareClass = cls;
interesting = true;
}
return interesting;
}
@@ -227,6 +227,7 @@ public class DefaultContainer implements SpiContainer {
bootupClasses.addPersistListeners(serverConfig.getPersistListeners());
bootupClasses.addQueryAdapters(serverConfig.getQueryAdapters());
bootupClasses.addServerConfigStartup(serverConfig.getServerConfigStartupListeners());
bootupClasses.addChangeLogInstances(serverConfig);
// run any ServerConfigStartup instances
bootupClasses.runServerConfigStartup(serverConfig);
@@ -204,7 +204,7 @@ public final class DefaultServer implements SpiEbeanServer {
this.serverCacheManager = cache;
this.databasePlatform = config.getDatabasePlatform();
this.backgroundExecutor = config.getBackgroundExecutor();
this.serverName = serverConfig.getName();
this.diffHelp = new DiffHelp(serverConfig.isDiffFlatMode());
this.lazyLoadBatchSize = serverConfig.getLazyLoadBatchSize();
@@ -0,0 +1,102 @@
package com.avaje.ebeaninternal.server.core;
import com.avaje.ebean.ValuePair;
import com.avaje.ebean.bean.EntityBean;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.BeanProperty;
import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocOne;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Helper to perform a 'diff' for an insert.
* <p>
* This intentionally does not include any OneToMany or ManyToMany properties.
* </p>
*/
public class DiffHelpInsert {
private DiffHelpInsert() {
}
/**
* Return a map of the differences between a and b.
* <p>
* A and B must be of the same type. B can be null, in which case the 'dirty
* diff' of a is returned.
* </p>
* <p>
* This intentionally does not include as OneToMany or ManyToMany properties.
* </p>
*/
public static Map<String, ValuePair> diff(EntityBean newBean, BeanDescriptor<?> desc) {
Map<String, ValuePair> map = new LinkedHashMap<String, ValuePair>();
diff(null, map, newBean, desc);
return map;
}
private static void diff(String prefix, Map<String, ValuePair> map, EntityBean newBean, BeanDescriptor<?> desc) {
// check the simple properties
BeanProperty[] base = desc.propertiesBaseScalar();
for (int i = 0; i < base.length; i++) {
Object newVal = (newBean == null) ? null : base[i].getValue(newBean);
if (newVal != null) {
String propName = (prefix == null) ? base[i].getName() : prefix + base[i].getName();
map.put(propName, new ValuePair(newVal, null));
}
}
diffAssocOne(prefix, newBean, desc, map);
diffEmbedded(prefix, newBean, desc, map);
}
/**
* Check the Embedded bean properties for differences.
* <p>
* If ANY of the properties are different then the whole Embedded bean is
* determined to be different as is added to the map.
* </p>
*/
private static void diffEmbedded(String prefix, EntityBean newBean, BeanDescriptor<?> desc, Map<String, ValuePair> map) {
BeanPropertyAssocOne<?>[] emb = desc.propertiesEmbedded();
for (int i = 0; i < emb.length; i++) {
EntityBean newVal = (EntityBean) emb[i].getValue(newBean);
if (newVal != null) {
String propName = (prefix == null) ? emb[i].getName() : prefix + emb[i].getName();
BeanDescriptor<?> embDesc = emb[i].getTargetDescriptor();
diff(propName + ".", map, newVal, embDesc);
}
}
}
/**
* If the properties are different by null OR if the id value is different,
* then add the Assoc One bean to the map.
*/
private static void diffAssocOne(String prefix, EntityBean newBean, BeanDescriptor<?> desc, Map<String, ValuePair> map) {
BeanPropertyAssocOne<?>[] ones = desc.propertiesOne();
for (int i = 0; i < ones.length; i++) {
Object newVal = ones[i].getValue(newBean);
if (newVal != null) {
BeanDescriptor<?> oneDesc = ones[i].getTargetDescriptor();
Object newId = oneDesc.getId((EntityBean)newVal);
if (newId != null) {
String propName = (prefix == null) ? ones[i].getName() : prefix + ones[i].getName();
String idName = oneDesc.getIdProperty().getName();
map.put(propName + "." + idName, new ValuePair(newId, null));
}
}
}
}
}
@@ -176,7 +176,7 @@ public class InternalConfiguration {
public JsonContext createJsonContext(SpiEbeanServer server) {
return new DJsonContext(server, jsonFactory);
return new DJsonContext(server, jsonFactory, typeManager);
}
public XmlConfig getXmlConfig() {
@@ -7,6 +7,7 @@ import com.avaje.ebean.bean.EntityBeanIntercept;
import com.avaje.ebean.event.BeanPersistController;
import com.avaje.ebean.event.BeanPersistListener;
import com.avaje.ebean.event.BeanPersistRequest;
import com.avaje.ebean.event.changelog.BeanChange;
import com.avaje.ebeaninternal.api.DerivedRelationshipData;
import com.avaje.ebeaninternal.api.SpiEbeanServer;
import com.avaje.ebeaninternal.api.SpiTransaction;
@@ -529,11 +530,20 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
beanDescriptor.cacheHandleDelete(idValue, this);
}
private void changeLog() {
BeanChange changeLogBean = beanDescriptor.getChangeLogBean(this);
if (changeLogBean != null) {
transaction.addBeanChange(changeLogBean);
}
}
/**
* Post processing.
*/
public void postExecute() {
changeLog();
if (controller != null) {
controllerPost();
}
@@ -2,6 +2,7 @@ package com.avaje.ebeaninternal.server.deploy;
import com.avaje.ebean.SqlUpdate;
import com.avaje.ebean.Transaction;
import com.avaje.ebean.ValuePair;
import com.avaje.ebean.annotation.ConcurrencyMode;
import com.avaje.ebean.bean.BeanCollection;
import com.avaje.ebean.bean.EntityBean;
@@ -15,6 +16,9 @@ import com.avaje.ebean.event.BeanPersistController;
import com.avaje.ebean.event.BeanPersistListener;
import com.avaje.ebean.event.BeanPostLoad;
import com.avaje.ebean.event.BeanQueryAdapter;
import com.avaje.ebean.event.changelog.BeanChange;
import com.avaje.ebean.event.changelog.ChangeLogFilter;
import com.avaje.ebean.event.changelog.ChangeType;
import com.avaje.ebean.meta.MetaBeanInfo;
import com.avaje.ebean.meta.MetaQueryPlanStatistic;
import com.avaje.ebeaninternal.api.HashQueryPlan;
@@ -26,7 +30,9 @@ import com.avaje.ebeaninternal.api.TransactionEventTable.TableIUD;
import com.avaje.ebeaninternal.server.cache.CachedBeanData;
import com.avaje.ebeaninternal.server.core.CacheOptions;
import com.avaje.ebeaninternal.server.core.DefaultSqlUpdate;
import com.avaje.ebeaninternal.server.core.DiffHelpInsert;
import com.avaje.ebeaninternal.server.core.InternString;
import com.avaje.ebeaninternal.server.core.PersistRequest;
import com.avaje.ebeaninternal.server.core.PersistRequestBean;
import com.avaje.ebeaninternal.server.deploy.id.IdBinder;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanDescriptor;
@@ -175,6 +181,11 @@ public class BeanDescriptor<T> implements MetaBeanInfo {
*/
private final BeanFindController beanFinder;
/**
* Used for fine grain filtering for the change log.
*/
private final ChangeLogFilter changeLogFilter;
/**
* The table joins for this bean.
*/
@@ -337,6 +348,7 @@ public class BeanDescriptor<T> implements MetaBeanInfo {
this.persistListener = deploy.getPersistListener();
this.beanPostLoad = deploy.getPostLoad();
this.queryAdapter = deploy.getQueryAdapter();
this.changeLogFilter = deploy.getChangeLogFilter();
this.defaultSelectClause = deploy.getDefaultSelectClause();
this.defaultSelectClauseSet = deploy.parseDefaultSelectClause(defaultSelectClause);
@@ -421,7 +433,7 @@ public class BeanDescriptor<T> implements MetaBeanInfo {
this.unloadProperties = derivePropertiesToUnload(prototypeEntityBean);
}
}
/**
* Derive an array of property positions for properties that are initialised in the constructor.
* These properties need to be unloaded when populating beans for queries.
@@ -598,6 +610,60 @@ public class BeanDescriptor<T> implements MetaBeanInfo {
}
}
/**
* Return true if this request should be included in the change log.
*/
public BeanChange getChangeLogBean(PersistRequestBean<T> request) {
if (changeLogFilter == null) {
return null;
}
PersistRequest.Type type = request.getType();
switch (type) {
case INSERT:
return changeLogFilter.includeInsert(request) ? insertBeanChange(request): null;
case UPDATE:
return changeLogFilter.includeUpdate(request) ? updateBeanChange(request): null;
case DELETE:
return changeLogFilter.includeDelete(request) ? deleteBeanChange(request) :null;
default:
throw new IllegalStateException("Unhandled request type " + type);
}
}
/**
* Return the bean change for a delete.
*/
@SuppressWarnings("unchecked")
private BeanChange deleteBeanChange(PersistRequestBean<T> request) {
return newBeanChange(request.getBeanId(), ChangeType.DELETE, Collections.EMPTY_MAP);
}
/**
* Return the bean change for an update.
*/
private BeanChange updateBeanChange(PersistRequestBean<T> request) {
return newBeanChange(request.getBeanId(), ChangeType.UPDATE, request.getEntityBeanIntercept().getDirtyValues());
}
/**
* Return the bean change for an insert.
*/
private BeanChange insertBeanChange(PersistRequestBean<T> request) {
return newBeanChange(request.getBeanId(), ChangeType.INSERT, insertDiff(request.getEntityBean()));
}
private BeanChange newBeanChange(Object id, ChangeType changeType, Map<String, ValuePair> values) {
return new BeanChange(getBaseTable(), id, changeType, values);
}
/**
* For insert we create a Map of ValuePair to have the same structure as update.
*/
private Map<String, ValuePair> insertDiff(EntityBean entityBean) {
return DiffHelpInsert.diff(entityBean, this);
}
/**
* Initialise the cache once the server has started.
*/
@@ -16,6 +16,10 @@ import com.avaje.ebean.config.dbplatform.DbHistorySupport;
import com.avaje.ebean.config.dbplatform.DbIdentity;
import com.avaje.ebean.config.dbplatform.IdGenerator;
import com.avaje.ebean.config.dbplatform.IdType;
import com.avaje.ebean.event.changelog.ChangeLogFilter;
import com.avaje.ebean.event.changelog.ChangeLogListener;
import com.avaje.ebean.event.changelog.ChangeLogPrepare;
import com.avaje.ebean.event.changelog.ChangeLogRegister;
import com.avaje.ebeaninternal.api.SpiEbeanServer;
import com.avaje.ebeaninternal.api.TransactionEventTable;
import com.avaje.ebeaninternal.server.core.BootupClasses;
@@ -105,6 +109,12 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
private final BeanManagerFactory beanManagerFactory;
private final ChangeLogListener changeLogListener;
private final ChangeLogRegister changeLogRegister;
private final ChangeLogPrepare changeLogPrepare;
private int enhancedClassCount;
private final boolean updateChangesOnly;
@@ -199,6 +209,9 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
this.reflectFactory = createReflectionFactory();
this.transientProperties = new TransientProperties();
this.changeLogPrepare = bootupClasses.getChangeLogPrepare();
this.changeLogListener = bootupClasses.getChangeLogListener();
this.changeLogRegister = bootupClasses.getChangeLogRegister();
}
/**
@@ -1003,6 +1016,14 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
persistListenerManager.addPersistListeners(descriptor);
beanQueryAdapterManager.addQueryAdapter(descriptor);
beanFinderManager.addFindControllers(descriptor);
if (changeLogRegister != null) {
ChangeLogFilter changeFilter = changeLogRegister.getChangeFilter(descriptor.getBeanType());
if (changeFilter != null) {
descriptor.setChangeLogFilter(changeFilter);
}
}
}
/**
@@ -1451,7 +1472,22 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
}
return true;
}
/**
* Return the changeLogPrepare (for setting user context into the ChangeSet
* in the foreground thread).
*/
public ChangeLogPrepare getChangeLogPrepare() {
return changeLogPrepare;
}
/**
* Return the changeLogListener (that actually does the logging).
*/
public ChangeLogListener getChangeLogListener() {
return changeLogListener;
}
/**
* Comparator to sort the BeanDescriptors by name.
*/
@@ -9,6 +9,7 @@ import com.avaje.ebean.event.BeanPersistController;
import com.avaje.ebean.event.BeanPersistListener;
import com.avaje.ebean.event.BeanPostLoad;
import com.avaje.ebean.event.BeanQueryAdapter;
import com.avaje.ebean.event.changelog.ChangeLogFilter;
import com.avaje.ebeaninternal.server.core.CacheOptions;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor.EntityType;
import com.avaje.ebeaninternal.server.deploy.ChainedBeanPersistController;
@@ -154,6 +155,8 @@ public class DeployBeanDescriptor<T> {
private boolean processedRawSqlExtend;
private ChangeLogFilter changeLogFilter;
/**
* Construct the BeanDescriptor.
*/
@@ -305,6 +308,14 @@ public class DeployBeanDescriptor<T> {
return beanType;
}
public void setChangeLogFilter(ChangeLogFilter changeLogFilter) {
this.changeLogFilter = changeLogFilter;
}
public ChangeLogFilter getChangeLogFilter() {
return changeLogFilter;
}
/**
* Returns the Inheritance mapping information. This will be null if this type
* of bean is not involved in any ORM inheritance mapping.
@@ -6,6 +6,7 @@ import com.avaje.ebean.text.json.*;
import com.avaje.ebean.text.PathProperties;
import com.avaje.ebeaninternal.api.SpiEbeanServer;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import com.avaje.ebeaninternal.server.type.TypeManager;
import com.avaje.ebeaninternal.util.ParamTypeHelper;
import com.avaje.ebeaninternal.util.ParamTypeHelper.ManyType;
import com.avaje.ebeaninternal.util.ParamTypeHelper.TypeInfo;
@@ -25,17 +26,24 @@ public class DJsonContext implements JsonContext {
private final JsonFactory jsonFactory;
private final TypeManager typeManager;
private final Object defaultObjectMapper;
private final JsonConfig.Include defaultInclude;
public DJsonContext(SpiEbeanServer server, JsonFactory jsonFactory) {
public DJsonContext(SpiEbeanServer server, JsonFactory jsonFactory, TypeManager typeManager) {
this.server = server;
this.typeManager = typeManager;
this.jsonFactory = (jsonFactory != null) ? jsonFactory : new JsonFactory();
this.defaultObjectMapper = this.server.getServerConfig().getObjectMapper();
this.defaultInclude = this.server.getServerConfig().getJsonInclude();
}
public JsonScalar getScalar(JsonGenerator generator) {
return new DefaultJsonScalar(typeManager, new WriteJson(generator, defaultInclude));
}
public boolean isSupportedType(Type genericType) {
return server.isSupportedType(genericType);
}
@@ -0,0 +1,38 @@
package com.avaje.ebeaninternal.server.text.json;
import com.avaje.ebean.text.json.JsonScalar;
import com.avaje.ebeaninternal.server.type.ScalarType;
import com.avaje.ebeaninternal.server.type.TypeManager;
import java.io.IOException;
/**
* Default implementation of JsonScalar.
*/
public class DefaultJsonScalar implements JsonScalar {
private final TypeManager typeManager;
private final WriteJson writeJson;
public DefaultJsonScalar(TypeManager typeManager, WriteJson writeJson) {
this.typeManager = typeManager;
this.writeJson = writeJson;
}
@Override
public void write(String name, Object value) throws IOException {
if (value instanceof String) {
writeJson.writeStringField(name, (String)value);
} else {
ScalarType scalarType = (ScalarType)typeManager.getScalarType(value.getClass());
if (scalarType == null) {
throw new IllegalArgumentException("unhandled type " + value.getClass());
}
scalarType.jsonWrite(writeJson, name, value);
}
}
}
@@ -26,27 +26,38 @@ public class WriteJson implements JsonWriter {
private final PathProperties pathProperties;
private final PathStack pathStack = new PathStack();
private final PathStack pathStack;
private final ArrayStack<Object> parentBeans = new ArrayStack<Object>();
private final ArrayStack<Object> parentBeans;
private final Object objectMapper;
private final JsonConfig.Include include;
/**
* Construct for full bean use (normal).
*/
public WriteJson(SpiEbeanServer server, JsonGenerator generator, PathProperties pathProperties, Object objectMapper, JsonConfig.Include include){
this.server = server;
this.generator = generator;
this.pathProperties = pathProperties;
this.objectMapper = objectMapper;
this.include = include;
this.parentBeans = new ArrayStack<Object>();
this.pathStack = new PathStack();
}
/**
* Construct for testing purposes only.
* Construct for Json scalar use.
*/
public WriteJson(JsonGenerator generator, JsonConfig.Include include) {
this(null, generator, null, null, include);
public WriteJson(JsonGenerator generator, JsonConfig.Include include){
this.generator = generator;
this.include = include;
this.server = null;
this.pathProperties = null;
this.objectMapper = null;
this.parentBeans = null;
this.pathStack = null;
}
/**
@@ -3,6 +3,9 @@ package com.avaje.ebeaninternal.server.transaction;
import com.avaje.ebean.TransactionCallback;
import com.avaje.ebean.bean.PersistenceContext;
import com.avaje.ebean.config.PersistBatch;
import com.avaje.ebean.config.dbplatform.DatabasePlatform.OnQueryOnly;
import com.avaje.ebean.event.changelog.BeanChange;
import com.avaje.ebean.event.changelog.ChangeSet;
import com.avaje.ebeaninternal.api.DerivedRelationshipData;
import com.avaje.ebeaninternal.api.SpiTransaction;
import com.avaje.ebeaninternal.api.TransactionEvent;
@@ -10,7 +13,6 @@ import com.avaje.ebeaninternal.server.core.PersistRequest;
import com.avaje.ebeaninternal.server.core.PersistRequestBean;
import com.avaje.ebeaninternal.server.lib.util.Str;
import com.avaje.ebeaninternal.server.persist.BatchControl;
import com.avaje.ebean.config.dbplatform.DatabasePlatform.OnQueryOnly;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -19,7 +21,12 @@ import javax.persistence.RollbackException;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
/**
* JDBC Connection based transaction.
@@ -133,6 +140,8 @@ public class JdbcTransaction implements SpiTransaction {
protected boolean batchOnCascadeSet;
protected TChangeLogHolder changeLogHolder;
/**
* Create a new JdbcTransaction.
*/
@@ -180,6 +189,19 @@ public class JdbcTransaction implements SpiTransaction {
return logPrefix;
}
@Override
public void addBeanChange(BeanChange beanChange) {
if (changeLogHolder == null) {
changeLogHolder = new TChangeLogHolder(this, 100);
}
changeLogHolder.addBeanChange(beanChange);
}
@Override
public void sendChangeLog(ChangeSet changesRequest) {
manager.sendChangeLog(changesRequest);
}
@Override
public void register(TransactionCallback callback) {
if (callbackList == null) {
@@ -210,6 +232,9 @@ public class JdbcTransaction implements SpiTransaction {
}
}
}
if (changeLogHolder != null) {
changeLogHolder.postRollback();
}
}
protected void firePreCommit() {
@@ -234,6 +259,9 @@ public class JdbcTransaction implements SpiTransaction {
}
}
}
if (changeLogHolder != null) {
changeLogHolder.postCommit();
}
}
@Override
@@ -0,0 +1,88 @@
package com.avaje.ebeaninternal.server.transaction;
import com.avaje.ebean.event.changelog.BeanChange;
import com.avaje.ebean.event.changelog.ChangeSet;
import com.avaje.ebean.event.changelog.TxnState;
import com.avaje.ebeaninternal.api.SpiTransaction;
import java.util.UUID;
/**
* Holder of the changes handling the case when we send the changes
* prior to commit or rollback as we hit the allowed 'batch size'.
*/
public class TChangeLogHolder {
/**
* The owning transaction.
*/
private final SpiTransaction owner;
/**
* A transaction id that can be used to join many changeSets when
* we send multiple for a large transaction.
*/
private final String transactionId;
/**
* When we hit batch size then send the changeSet even when the tranaction
* has not yet completed.
*/
private final int batchSize;
/**
* The changes we collect to send to the listener.
*/
private ChangeSet changes;
private long batchId;
/**
* Counter to check when we hit the batch size.
*/
private int count;
/**
* Construct with the owning transaction and batch size to use.
*/
public TChangeLogHolder(SpiTransaction owner, int batchSize) {
this.owner = owner;
this.transactionId = UUID.randomUUID().toString();
this.batchSize = batchSize;
this.changes = new ChangeSet(transactionId, 0);
}
/**
* Add a bean change to the change set.
*/
public void addBeanChange(BeanChange change) {
changes.addBeanChange(change);
if (++count >= batchSize) {
// we hit the batch size so send what we have knowing
// that the transaction has not completed yet and
// reset the changes and count
owner.sendChangeLog(changes);
changes = new ChangeSet(transactionId, ++batchId);
count = 0;
}
}
/**
* On post commit send the changes we have collected.
*/
public void postCommit() {
changes.setTxnState(TxnState.COMMITTED);
owner.sendChangeLog(changes);
}
/**
* On post rollback send the changes we have collected and
* leave it up to the listener to decide what to do.
*/
public void postRollback() {
changes.setTxnState(TxnState.ROLLBACK);
owner.sendChangeLog(changes);
}
}
@@ -6,6 +6,9 @@ import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.config.dbplatform.DatabasePlatform.OnQueryOnly;
import com.avaje.ebean.dbmigration.DbOffline;
import com.avaje.ebean.event.TransactionEventListener;
import com.avaje.ebean.event.changelog.ChangeLogListener;
import com.avaje.ebean.event.changelog.ChangeLogPrepare;
import com.avaje.ebean.event.changelog.ChangeSet;
import com.avaje.ebeaninternal.api.SpiTransaction;
import com.avaje.ebeaninternal.api.TransactionEvent;
import com.avaje.ebeaninternal.api.TransactionEventTable;
@@ -80,6 +83,17 @@ public class TransactionManager {
protected final TransactionEventListener[] transactionEventListeners;
/**
* Used to prepare the change set setting user context information in the
* foreground thread before logging.
*/
private final ChangeLogPrepare changeLogPrepare;
/**
* Performs the actual logging of the change set in background.
*/
private final ChangeLogListener changeLogListener;
/**
* Create the TransactionManager
*/
@@ -89,6 +103,8 @@ public class TransactionManager {
this.persistBatch = config.getPersistBatch();
this.persistBatchOnCascade = config.appliedPersistBatchOnCascade();
this.beanDescriptorManager = descMgr;
this.changeLogPrepare = descMgr.getChangeLogPrepare();
this.changeLogListener = descMgr.getChangeLogListener();
this.clusterManager = clusterManager;
this.serverName = config.getName();
this.backgroundExecutor = backgroundExecutor;
@@ -439,4 +455,21 @@ public class TransactionManager {
}
}
/**
* Prepare and then send/log the changeSet.
*/
public void sendChangeLog(final ChangeSet changeSet) {
// can set userId, userIpAddress & userContext if desired
if (changeLogPrepare.prepare(changeSet)) {
// call the log method in background
backgroundExecutor.execute(new Runnable() {
@Override
public void run() {
changeLogListener.log(changeSet);
}
});
}
}
}