From 84190f3c2aba4680764bd577e3b637171fd08b1f Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Thu, 20 Aug 2015 20:46:28 +1200 Subject: [PATCH] #390 - ENH: Add Change log mechanism for easy fine grained logging of insert, update, delete activity --- .../com/avaje/ebean/annotation/ChangeLog.java | 21 ++ .../com/avaje/ebean/config/ServerConfig.java | 65 ++++++ .../ebean/event/changelog/BeanChange.java | 123 +++++++++++ .../event/changelog/ChangeLogFilter.java | 25 +++ .../event/changelog/ChangeLogListener.java | 21 ++ .../event/changelog/ChangeLogPrepare.java | 27 +++ .../event/changelog/ChangeLogRegister.java | 18 ++ .../ebean/event/changelog/ChangeSet.java | 202 ++++++++++++++++++ .../ebean/event/changelog/ChangeType.java | 32 +++ .../avaje/ebean/event/changelog/TxnState.java | 40 ++++ .../avaje/ebean/text/json/JsonContext.java | 11 + .../com/avaje/ebean/text/json/JsonScalar.java | 11 + .../ebeaninternal/api/ScopedTransaction.java | 12 ++ .../ebeaninternal/api/SpiEbeanServer.java | 2 +- .../ebeaninternal/api/SpiTransaction.java | 11 + .../server/core/BootupClasses.java | 197 +++++++++++------ .../server/core/DefaultContainer.java | 1 + .../server/core/DefaultServer.java | 2 +- .../server/core/DiffHelpInsert.java | 102 +++++++++ .../server/core/InternalConfiguration.java | 2 +- .../server/core/PersistRequestBean.java | 10 + .../server/deploy/BeanDescriptor.java | 68 +++++- .../server/deploy/BeanDescriptorManager.java | 38 +++- .../deploy/meta/DeployBeanDescriptor.java | 11 + .../server/text/json/DJsonContext.java | 10 +- .../server/text/json/DefaultJsonScalar.java | 38 ++++ .../server/text/json/WriteJson.java | 21 +- .../server/transaction/JdbcTransaction.java | 32 ++- .../server/transaction/TChangeLogHolder.java | 88 ++++++++ .../transaction/TransactionManager.java | 33 +++ .../server/core/TestDiffHelpInsertSimple.java | 66 ++++++ .../core/TestDiffHelpInsertWithEmbedded.java | 107 ++++++++++ .../avaje/tests/changelog/TestChangeLog.java | 159 ++++++++++++++ .../tests/model/basic/EBasicChangeLog.java | 120 +++++++++++ 34 files changed, 1642 insertions(+), 84 deletions(-) create mode 100644 src/main/java/com/avaje/ebean/annotation/ChangeLog.java create mode 100644 src/main/java/com/avaje/ebean/event/changelog/BeanChange.java create mode 100644 src/main/java/com/avaje/ebean/event/changelog/ChangeLogFilter.java create mode 100644 src/main/java/com/avaje/ebean/event/changelog/ChangeLogListener.java create mode 100644 src/main/java/com/avaje/ebean/event/changelog/ChangeLogPrepare.java create mode 100644 src/main/java/com/avaje/ebean/event/changelog/ChangeLogRegister.java create mode 100644 src/main/java/com/avaje/ebean/event/changelog/ChangeSet.java create mode 100644 src/main/java/com/avaje/ebean/event/changelog/ChangeType.java create mode 100644 src/main/java/com/avaje/ebean/event/changelog/TxnState.java create mode 100644 src/main/java/com/avaje/ebean/text/json/JsonScalar.java create mode 100644 src/main/java/com/avaje/ebeaninternal/server/core/DiffHelpInsert.java create mode 100644 src/main/java/com/avaje/ebeaninternal/server/text/json/DefaultJsonScalar.java create mode 100644 src/main/java/com/avaje/ebeaninternal/server/transaction/TChangeLogHolder.java create mode 100644 src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpInsertSimple.java create mode 100644 src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpInsertWithEmbedded.java create mode 100644 src/test/java/com/avaje/tests/changelog/TestChangeLog.java create mode 100644 src/test/java/com/avaje/tests/model/basic/EBasicChangeLog.java diff --git a/src/main/java/com/avaje/ebean/annotation/ChangeLog.java b/src/main/java/com/avaje/ebean/annotation/ChangeLog.java new file mode 100644 index 000000000..97e01639c --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/ChangeLog.java @@ -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; + +} diff --git a/src/main/java/com/avaje/ebean/config/ServerConfig.java b/src/main/java/com/avaje/ebean/config/ServerConfig.java index d07da042c..d93f52b36 100644 --- a/src/main/java/com/avaje/ebean/config/ServerConfig.java +++ b/src/main/java/com/avaje/ebean/config/ServerConfig.java @@ -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 configStartupListeners = new ArrayList(); private List transactionEventListeners = new ArrayList(); + 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. + *

+ * This is used to set user context information to the ChangeSet in the + * foreground thread prior to the logging occurring in a background thread. + *

+ */ + public ChangeLogPrepare getChangeLogPrepare() { + return changeLogPrepare; + } + + /** + * Set the ChangeLogPrepare. + *

+ * This is used to set user context information to the ChangeSet in the + * foreground thread prior to the logging occurring in a background thread. + *

+ */ + 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. */ diff --git a/src/main/java/com/avaje/ebean/event/changelog/BeanChange.java b/src/main/java/com/avaje/ebean/event/changelog/BeanChange.java new file mode 100644 index 000000000..5e69cacb7 --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/changelog/BeanChange.java @@ -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 values; + + /** + * Construct with all the details. + */ + public BeanChange(String table, Object id, ChangeType type, Map 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 getValues() { + return values; + } + + /** + * Set the value pairs (for JSON tools). + */ + public void setValues(Map values) { + this.values = values; + } +} diff --git a/src/main/java/com/avaje/ebean/event/changelog/ChangeLogFilter.java b/src/main/java/com/avaje/ebean/event/changelog/ChangeLogFilter.java new file mode 100644 index 000000000..2451b9e4d --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/changelog/ChangeLogFilter.java @@ -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); + +} diff --git a/src/main/java/com/avaje/ebean/event/changelog/ChangeLogListener.java b/src/main/java/com/avaje/ebean/event/changelog/ChangeLogListener.java new file mode 100644 index 000000000..7049088cc --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/changelog/ChangeLogListener.java @@ -0,0 +1,21 @@ +package com.avaje.ebean.event.changelog; + +/** + * Listen for changes. + *

+ * Implementations can take the changes and store them in a document store for auditing purposes etc. + *

+ */ +public interface ChangeLogListener { + + /** + * Log the batch of changes. + *

+ * 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 COMMITTED or ROLLBACK. + */ + void log(ChangeSet changeSet); + +} diff --git a/src/main/java/com/avaje/ebean/event/changelog/ChangeLogPrepare.java b/src/main/java/com/avaje/ebean/event/changelog/ChangeLogPrepare.java new file mode 100644 index 000000000..ba9634203 --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/changelog/ChangeLogPrepare.java @@ -0,0 +1,27 @@ +package com.avaje.ebean.event.changelog; + +/** + * Listen for changes. + *

+ * Implementations can take the changes and store them in a document store for auditing purposes etc. + *

+ */ +public interface ChangeLogPrepare { + + /** + * In the foreground prepare the changeLog for sending. + *

+ * This is intended to set extra context information onto the ChangeSet such + * as the application user id and client ip address. + *

+ *

+ * 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. + *

+ * + * @return true if the changeLog should then be sent to the log method in a background thread. + */ + boolean prepare(ChangeSet changeSet); + +} diff --git a/src/main/java/com/avaje/ebean/event/changelog/ChangeLogRegister.java b/src/main/java/com/avaje/ebean/event/changelog/ChangeLogRegister.java new file mode 100644 index 000000000..a9b7cbe62 --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/changelog/ChangeLogRegister.java @@ -0,0 +1,18 @@ +package com.avaje.ebean.event.changelog; + +/** + * Used to assign ChangeLogFilters to bean types. + *

+ * Ebean has a built in implementation that uses the ChangeLog annotation to build + * appropriate ChangeLogFilters but you can provide an implementation to use instead. + *

+ */ +public interface ChangeLogRegister { + + /** + * For the given bean type return the Change log filter to use. + *

+ * This filter provides control over which persist request are included in the change log. + */ + ChangeLogFilter getChangeFilter(Class beanType); +} diff --git a/src/main/java/com/avaje/ebean/event/changelog/ChangeSet.java b/src/main/java/com/avaje/ebean/event/changelog/ChangeSet.java new file mode 100644 index 000000000..e04fb2f09 --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/changelog/ChangeSet.java @@ -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 changes = new ArrayList(); + + /** + * 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. + *

+ * This can be set by the ChangeLogListener in the prepare() method which is called + * in the foreground thread. + *

+ */ + 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. + *

+ * This can be set by the ChangeLogListener in the prepare() method which is called + * in the foreground thread. + *

+ */ + 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). + *

+ * This can be set by the ChangeLogListener in the prepare() method which is called + * in the foreground thread. + *

+ */ + public void setUserContext(String userContext) { + this.userContext = userContext; + } + + /** + * Return the bean changes. + */ + public List getChanges() { + return changes; + } + + /** + * Set the bean changes (used by JSON tools). + */ + public void setChanges(List changes) { + this.changes = changes; + } +} diff --git a/src/main/java/com/avaje/ebean/event/changelog/ChangeType.java b/src/main/java/com/avaje/ebean/event/changelog/ChangeType.java new file mode 100644 index 000000000..0f5170d4e --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/changelog/ChangeType.java @@ -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; + } +} diff --git a/src/main/java/com/avaje/ebean/event/changelog/TxnState.java b/src/main/java/com/avaje/ebean/event/changelog/TxnState.java new file mode 100644 index 000000000..286c1701e --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/changelog/TxnState.java @@ -0,0 +1,40 @@ +package com.avaje.ebean.event.changelog; + +/** + * Transaction state when ChangeSets are sent to the ChangeSetListener. + *

+ * 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. + *

+ */ +public enum TxnState { + + /** + * The Transaction is still in progress. + *

+ * 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; + } +} diff --git a/src/main/java/com/avaje/ebean/text/json/JsonContext.java b/src/main/java/com/avaje/ebean/text/json/JsonContext.java index f5f52538c..fee9e65bf 100644 --- a/src/main/java/com/avaje/ebean/text/json/JsonContext.java +++ b/src/main/java/com/avaje/ebean/text/json/JsonContext.java @@ -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. + *

+ * 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. + *

+ */ + JsonScalar getScalar(JsonGenerator generator); + } \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/text/json/JsonScalar.java b/src/main/java/com/avaje/ebean/text/json/JsonScalar.java new file mode 100644 index 000000000..f1cd9cc2f --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/json/JsonScalar.java @@ -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; +} diff --git a/src/main/java/com/avaje/ebeaninternal/api/ScopedTransaction.java b/src/main/java/com/avaje/ebeaninternal/api/ScopedTransaction.java index 6b197b892..788317eeb 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/ScopedTransaction.java +++ b/src/main/java/com/avaje/ebeaninternal/api/ScopedTransaction.java @@ -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); diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiEbeanServer.java b/src/main/java/com/avaje/ebeaninternal/api/SpiEbeanServer.java index 7d1412f96..ca2be019e 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiEbeanServer.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiEbeanServer.java @@ -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. */ diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiTransaction.java b/src/main/java/com/avaje/ebeaninternal/api/SpiTransaction.java index 8cdb889a2..1b36a0ef9 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiTransaction.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiTransaction.java @@ -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); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/BootupClasses.java b/src/main/java/com/avaje/ebeaninternal/server/core/BootupClasses.java index f6d0f3e00..256f36f2e 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/BootupClasses.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/BootupClasses.java @@ -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 queryAdapterInstances = new ArrayList(); private final List transactionEventListenerInstances = new ArrayList(); + 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 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. + *

+ * 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 void createAdd(Class cls, List 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 getBeanQueryAdapters() { + // add class registered BeanQueryAdapter to the already created instances + for (Class cls : beanQueryAdapterList) { + createAdd(cls, queryAdapterInstances); + } return queryAdapterInstances; } public List 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 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 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 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 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; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultContainer.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultContainer.java index 99213518b..8c2ef01d9 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultContainer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultContainer.java @@ -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); diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java index 366c7249c..fa74466d3 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java @@ -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(); diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DiffHelpInsert.java b/src/main/java/com/avaje/ebeaninternal/server/core/DiffHelpInsert.java new file mode 100644 index 000000000..f59f9f1c5 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DiffHelpInsert.java @@ -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. + *

+ * This intentionally does not include any OneToMany or ManyToMany properties. + *

+ */ +public class DiffHelpInsert { + + private DiffHelpInsert() { + } + + /** + * Return a map of the differences between a and b. + *

+ * A and B must be of the same type. B can be null, in which case the 'dirty + * diff' of a is returned. + *

+ *

+ * This intentionally does not include as OneToMany or ManyToMany properties. + *

+ */ + public static Map diff(EntityBean newBean, BeanDescriptor desc) { + + Map map = new LinkedHashMap(); + diff(null, map, newBean, desc); + return map; + } + + private static void diff(String prefix, Map 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. + *

+ * If ANY of the properties are different then the whole Embedded bean is + * determined to be different as is added to the map. + *

+ */ + private static void diffEmbedded(String prefix, EntityBean newBean, BeanDescriptor desc, Map 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 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)); + } + } + } + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java b/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java index 227f16435..2ee44017b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java @@ -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() { diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java b/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java index aacecbb62..c1d675ed7 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java @@ -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 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(); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java index 754c02a00..8d9b8a8d4 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java @@ -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 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 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 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 implements MetaBeanInfo { } } + /** + * Return true if this request should be included in the change log. + */ + public BeanChange getChangeLogBean(PersistRequestBean 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 request) { + return newBeanChange(request.getBeanId(), ChangeType.DELETE, Collections.EMPTY_MAP); + } + + /** + * Return the bean change for an update. + */ + private BeanChange updateBeanChange(PersistRequestBean request) { + return newBeanChange(request.getBeanId(), ChangeType.UPDATE, request.getEntityBeanIntercept().getDirtyValues()); + } + + /** + * Return the bean change for an insert. + */ + private BeanChange insertBeanChange(PersistRequestBean request) { + return newBeanChange(request.getBeanId(), ChangeType.INSERT, insertDiff(request.getEntityBean())); + } + + private BeanChange newBeanChange(Object id, ChangeType changeType, Map 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 insertDiff(EntityBean entityBean) { + return DiffHelpInsert.diff(entityBean, this); + } + /** * Initialise the cache once the server has started. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorManager.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorManager.java index 1778907c9..6f6410218 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -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. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java index 83f2acad9..67ee61d10 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java @@ -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 { private boolean processedRawSqlExtend; + private ChangeLogFilter changeLogFilter; + /** * Construct the BeanDescriptor. */ @@ -305,6 +308,14 @@ public class DeployBeanDescriptor { 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. diff --git a/src/main/java/com/avaje/ebeaninternal/server/text/json/DJsonContext.java b/src/main/java/com/avaje/ebeaninternal/server/text/json/DJsonContext.java index 00e49d017..f5159797b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/text/json/DJsonContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/text/json/DJsonContext.java @@ -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); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/text/json/DefaultJsonScalar.java b/src/main/java/com/avaje/ebeaninternal/server/text/json/DefaultJsonScalar.java new file mode 100644 index 000000000..46e64eb26 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/text/json/DefaultJsonScalar.java @@ -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); + } + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/text/json/WriteJson.java b/src/main/java/com/avaje/ebeaninternal/server/text/json/WriteJson.java index 9a5500dca..9e91ff67b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/text/json/WriteJson.java +++ b/src/main/java/com/avaje/ebeaninternal/server/text/json/WriteJson.java @@ -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 parentBeans = new ArrayStack(); + private final ArrayStack 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(); + 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; } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java b/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java index 3cc6714f4..3d1248099 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java +++ b/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java @@ -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 diff --git a/src/main/java/com/avaje/ebeaninternal/server/transaction/TChangeLogHolder.java b/src/main/java/com/avaje/ebeaninternal/server/transaction/TChangeLogHolder.java new file mode 100644 index 000000000..7ef2f7aa7 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/transaction/TChangeLogHolder.java @@ -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); + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/transaction/TransactionManager.java b/src/main/java/com/avaje/ebeaninternal/server/transaction/TransactionManager.java index 5a1131c92..59f20c76a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/transaction/TransactionManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/transaction/TransactionManager.java @@ -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); + } + }); + } + } } diff --git a/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpInsertSimple.java b/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpInsertSimple.java new file mode 100644 index 000000000..728468799 --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpInsertSimple.java @@ -0,0 +1,66 @@ +package com.avaje.ebeaninternal.server.core; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.EbeanServer; +import com.avaje.ebean.ValuePair; +import com.avaje.ebean.bean.EntityBean; +import com.avaje.ebeaninternal.api.SpiEbeanServer; +import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; +import com.avaje.tests.model.basic.Customer; +import com.avaje.tests.model.basic.Order; +import com.avaje.tests.model.basic.Order.Status; +import org.junit.Test; + +import java.sql.Date; +import java.sql.Timestamp; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class TestDiffHelpInsertSimple extends BaseTestCase { + + long firstTime = System.currentTimeMillis() - 10000; + + EbeanServer server; + + BeanDescriptor orderDesc; + + public TestDiffHelpInsertSimple() { + server = Ebean.getServer(null); + SpiEbeanServer spiServer = (SpiEbeanServer) server; + orderDesc = spiServer.getBeanDescriptor(Order.class); + } + + private Order createBaseOrder(EbeanServer server) { + Order order1 = new Order(); + order1.setId(12); + order1.setCretime(new Timestamp(firstTime)); + order1.setCustomer(server.getReference(Customer.class, 1234)); + order1.setStatus(Status.NEW); + //order1.setShipDate(new Date(firstTime)); + order1.setOrderDate(new Date(firstTime)); + return order1; + } + + @Test + public void basic() { + + Order order1 = createBaseOrder(server); + + Map diff = DiffHelpInsert.diff((EntityBean) order1, orderDesc); + + assertEquals(4, diff.size()); + + Set keySet = diff.keySet(); + assertTrue(keySet.contains("cretime")); + assertTrue(keySet.contains("status")); + assertTrue(keySet.contains("orderDate")); + assertTrue(keySet.contains("customer.id")); + assertFalse(keySet.contains("shipDate")); + } + +} diff --git a/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpInsertWithEmbedded.java b/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpInsertWithEmbedded.java new file mode 100644 index 000000000..6d220f730 --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpInsertWithEmbedded.java @@ -0,0 +1,107 @@ +package com.avaje.ebeaninternal.server.core; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.EbeanServer; +import com.avaje.ebean.ValuePair; +import com.avaje.ebean.bean.EntityBean; +import com.avaje.ebeaninternal.api.SpiEbeanServer; +import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; +import com.avaje.tests.model.embedded.EMain; +import com.avaje.tests.model.embedded.Eembeddable; +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class TestDiffHelpInsertWithEmbedded extends BaseTestCase { + + EbeanServer server; + + BeanDescriptor emainDesc; + + public TestDiffHelpInsertWithEmbedded() { + server = Ebean.getServer(null); + SpiEbeanServer spiServer = (SpiEbeanServer) server; + emainDesc = spiServer.getBeanDescriptor(EMain.class); + } + + @Test + public void simple() { + + EMain emain1 = createEMain(); + + Map diff = DiffHelpInsert.diff((EntityBean) emain1, emainDesc); + assertEquals(3, diff.size()); + assertEquals("foo", diff.get("name").getNewValue()); + assertEquals(13L, diff.get("version").getNewValue()); + + ValuePair valuePair = diff.get("embeddable.description"); + assertNotNull(valuePair); + assertEquals("bar", valuePair.getNewValue()); + assertNull(valuePair.getOldValue()); + } + + @Test + public void scalarPropertyAsNull() { + + EMain emain1 = createEMain(); + emain1.setName(null); + + Map diff = DiffHelpInsert.diff((EntityBean) emain1, emainDesc); + assertEquals(2, diff.size()); + assertNull(diff.get("name")); + assertEquals(13L, diff.get("version").getNewValue()); + + ValuePair valuePair = diff.get("embeddable.description"); + assertNotNull(valuePair); + assertEquals("bar", valuePair.getNewValue()); + assertNull(valuePair.getOldValue()); + } + + @Test + public void embeddedAsNull() { + + EMain emain1 = createEMain(); + emain1.setEmbeddable(null); + + Map diff = DiffHelpInsert.diff((EntityBean) emain1, emainDesc); + assertEquals(2, diff.size()); + assertEquals("foo", diff.get("name").getNewValue()); + assertEquals(13L, diff.get("version").getNewValue()); + + assertNull(diff.get("embeddable.description")); + } + + + @Test + public void embeddedPropertiesAsNull() { + + EMain emain1 = createEMain(); + emain1.getEmbeddable().setDescription(null); + + Map diff = DiffHelpInsert.diff((EntityBean) emain1, emainDesc); + assertEquals(2, diff.size()); + assertEquals("foo", diff.get("name").getNewValue()); + assertEquals(13L, diff.get("version").getNewValue()); + + assertNull(diff.get("embeddable.description")); + } + + private EMain createEMain() { + + EMain emain = new EMain(); + emain.setName("foo"); + emain.setVersion(13l); + + Eembeddable embeddable = new Eembeddable(); + embeddable.setDescription("bar"); + emain.setEmbeddable(embeddable); + + return emain; + } + +} diff --git a/src/test/java/com/avaje/tests/changelog/TestChangeLog.java b/src/test/java/com/avaje/tests/changelog/TestChangeLog.java new file mode 100644 index 000000000..5436117af --- /dev/null +++ b/src/test/java/com/avaje/tests/changelog/TestChangeLog.java @@ -0,0 +1,159 @@ +package com.avaje.tests.changelog; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.EbeanServerFactory; +import com.avaje.ebean.annotation.ChangeLog; +import com.avaje.ebean.config.ServerConfig; +import com.avaje.ebean.event.BeanPersistRequest; +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.ebean.event.changelog.ChangeSet; +import com.avaje.ebeaninternal.api.SpiEbeanServer; +import com.avaje.tests.model.basic.EBasicChangeLog; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TestChangeLog extends BaseTestCase { + + TDChangeLogPrepare changeLogPrepare = new TDChangeLogPrepare(); + + TDChangeLogListener changeLogListener = new TDChangeLogListener(); + + TDChangeLogRegister changeLogRegister = new TDChangeLogRegister(); + + SpiEbeanServer server; + + @Before + public void setup() { + server = getServer(); + } + + @After + public void shutdown() { + server.shutdown(true, false); + } + + @Test + public void test() { + + EBasicChangeLog bean = new EBasicChangeLog(); + bean.setName("logBean"); + bean.setShortDescription("hello"); + server.save(bean); + + bean.setName("ChangedName"); + server.save(bean); + + server.delete(bean); + + } + + private SpiEbeanServer getServer() { + + ServerConfig config = new ServerConfig(); + config.setName("h2other"); + config.loadFromProperties(); + + config.setDdlGenerate(true); + config.setDdlRun(true); + config.setDefaultServer(false); + config.setRegister(false); + + config.addClass(EBasicChangeLog.class); + + config.setChangeLogPrepare(changeLogPrepare); + config.setChangeLogListener(changeLogListener); + config.setChangeLogRegister(changeLogRegister); + + return (SpiEbeanServer) EbeanServerFactory.create(config); + } + + class TDChangeLogPrepare implements ChangeLogPrepare { + @Override + public boolean prepare(ChangeSet changes) { + changes.setUserId("appUser1"); + changes.setUserIpAddress("1.1.1.1"); + return true; + } + } + + class TDChangeLogListener implements ChangeLogListener { + + ObjectMapper objectMapper = new ObjectMapper(); + + ChangeSet changes; + + /** + */ + public TDChangeLogListener() { + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + + @Override + public void log(ChangeSet changes) { + this.changes = changes; + try { + String json = objectMapper.writeValueAsString(changes); + + logger.info(json); + + ChangeSet changes1 = objectMapper.readValue(json, ChangeSet.class); + + assertEquals(changes.getTxnId(), changes1.getTxnId()); + assertEquals(changes.getUserId(), changes1.getUserId()); + assertEquals(changes.getUserIpAddress(), changes1.getUserIpAddress()); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + } + + class TDChangeLogRegister implements ChangeLogRegister { + + @Override + public ChangeLogFilter getChangeFilter(Class beanType) { + + ChangeLog changeLog = beanType.getAnnotation(ChangeLog.class); + if (changeLog != null) { + return new TDFilter(); + } + + return null; + } + } + + + /** + * Change log filter that includes all the inserts, updates and deletes. + */ + class TDFilter implements ChangeLogFilter { + + /** + * Returns true including all the inserts. + */ + @Override + public boolean includeInsert(BeanPersistRequest insertRequest) { + return true; + } + + @Override + public boolean includeUpdate(BeanPersistRequest updateRequest) { + return true; + } + + @Override + public boolean includeDelete(BeanPersistRequest deleteRequest) { + return true; + } + } +} diff --git a/src/test/java/com/avaje/tests/model/basic/EBasicChangeLog.java b/src/test/java/com/avaje/tests/model/basic/EBasicChangeLog.java new file mode 100644 index 000000000..91ca2ff79 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/basic/EBasicChangeLog.java @@ -0,0 +1,120 @@ +package com.avaje.tests.model.basic; + +import com.avaje.ebean.annotation.ChangeLog; +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 javax.validation.constraints.Size; +import java.sql.Timestamp; + +/** + * + */ +@ChangeLog +@Entity +public class EBasicChangeLog { + + @Id + Long id; + + @Size(max = 20) + String name; + + @Size(max = 50) + String shortDescription; + + @Size(max = 100) + String longDescription; + + @WhoCreated + String whoCreated; + + @WhoModified + String whoModified; + + @WhenCreated + Timestamp whenCreated; + + @WhenModified + Timestamp whenModified; + + @Version + Long version; + + 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 String getShortDescription() { + return shortDescription; + } + + public void setShortDescription(String shortDescription) { + this.shortDescription = shortDescription; + } + + public String getLongDescription() { + return longDescription; + } + + public void setLongDescription(String longDescription) { + this.longDescription = longDescription; + } + + 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; + } + + 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 Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } +}