mirror of
https://github.com/ebean-orm/ebean.git
synced 2024-04-21 10:51:47 +00:00
#390 - ENH: Add Change log mechanism for easy fine grained logging of insert, update, delete activity.
This adds default implementations for register,filter, prepare and log of the change sets.
This commit is contained in:
@@ -8,7 +8,7 @@ import java.lang.annotation.Target;
|
||||
/**
|
||||
* Marks an entity bean as being included in the change logging.
|
||||
*/
|
||||
@Target({ ElementType.TYPE })
|
||||
@Target({ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ChangeLog {
|
||||
|
||||
@@ -18,4 +18,14 @@ public @interface ChangeLog {
|
||||
*/
|
||||
boolean excludeInserts() default false;
|
||||
|
||||
/**
|
||||
* When specified only include update requests that have at least one
|
||||
* of the given properties as a dirty property.
|
||||
* <p>
|
||||
* This provides a way to filter requests to include in the change log such that
|
||||
* only updates that include at least one of the given properties is included
|
||||
* in the change log.
|
||||
* </p>
|
||||
*/
|
||||
String[] updatesThatInclude() default {};
|
||||
}
|
||||
|
||||
@@ -581,7 +581,29 @@ public final class EntityBeanIntercept implements Serializable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return true if any of the given property names are dirty.
|
||||
*/
|
||||
public boolean hasDirtyProperty(Set<String> propertyNames) {
|
||||
|
||||
String[] names = owner._ebean_getPropertyNames();
|
||||
int len = getPropertyLength();
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (changedProps != null && changedProps[i]) {
|
||||
// the property has been changed on this bean
|
||||
if (propertyNames.contains(names[i])) {
|
||||
return true;
|
||||
}
|
||||
} else if (embeddedDirty != null && embeddedDirty[i]) {
|
||||
if (propertyNames.contains(names[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a map of dirty properties with their new and old values.
|
||||
*/
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.avaje.ebean.event;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.avaje.ebean.EbeanServer;
|
||||
import com.avaje.ebean.Transaction;
|
||||
import com.avaje.ebean.ValuePair;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Holds the information available for a bean persist (insert, update or
|
||||
* delete).
|
||||
@@ -34,9 +34,30 @@ public interface BeanPersistRequest<T> {
|
||||
|
||||
/**
|
||||
* For an update this is the set of properties that where updated.
|
||||
* <p>
|
||||
* Note that hasDirtyProperty() is a more efficient check than this method and
|
||||
* should be preferred if it satisfies the requirement.
|
||||
* </p>
|
||||
*/
|
||||
Set<String> getUpdatedProperties();
|
||||
|
||||
/**
|
||||
* Return true for an update request if at least one of dirty properties is contained
|
||||
* in the given set of property names.
|
||||
* <p>
|
||||
* This method will produce less GC compared with getUpdatedProperties() and should
|
||||
* be preferred if it satisfies the requirement.
|
||||
* </p>
|
||||
* <p>
|
||||
* Note that this method is used by the default ChangeLogFilter mechanism for when
|
||||
* the <code>@ChangeLog</code> updatesThatInclude attribute has been specified.
|
||||
* </p>
|
||||
*
|
||||
* @param propertyNames a set of property names which we are checking to see if at least
|
||||
* one of them is dirty.
|
||||
*/
|
||||
boolean hasDirtyProperty(Set<String> propertyNames);
|
||||
|
||||
/**
|
||||
* Returns the bean being inserted updated or deleted.
|
||||
*/
|
||||
@@ -45,6 +66,6 @@ public interface BeanPersistRequest<T> {
|
||||
/**
|
||||
* Returns a map of the properties that have changed and their new and old values.
|
||||
*/
|
||||
Map<String,ValuePair> getUpdatedValues();
|
||||
Map<String, ValuePair> getUpdatedValues();
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.avaje.ebean.plugin;
|
||||
|
||||
/**
|
||||
* A 'plugin' that wants to be configured on startup so it can use features of the EbeanServer itself.
|
||||
*/
|
||||
public interface SpiServerPlugin {
|
||||
|
||||
/**
|
||||
* Configure the plugin.
|
||||
*/
|
||||
void configure(SpiServer server);
|
||||
|
||||
/**
|
||||
* Called just before the server starts indicating if it is coming up in online mode.
|
||||
*/
|
||||
void online(boolean online);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package com.avaje.ebeaninternal.server.changelog;
|
||||
|
||||
import com.avaje.ebean.ValuePair;
|
||||
import com.avaje.ebean.event.changelog.BeanChange;
|
||||
import com.avaje.ebean.event.changelog.ChangeSet;
|
||||
import com.avaje.ebean.event.changelog.ChangeType;
|
||||
import com.avaje.ebean.text.json.JsonContext;
|
||||
import com.avaje.ebean.text.json.JsonScalar;
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Builds JSON appropriate for loading into ElasticS via the bulk API.
|
||||
*/
|
||||
public class BulkJsonBuilder {
|
||||
|
||||
protected final JsonFactory jsonFactory = new JsonFactory();
|
||||
|
||||
protected final String indexName;
|
||||
|
||||
protected final String indexType;
|
||||
|
||||
protected final JsonContext json;
|
||||
|
||||
protected BulkJsonBuilder(JsonContext json, String indexName, String indexType) {
|
||||
this.json = json;
|
||||
this.indexName = indexName;
|
||||
this.indexType = indexType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the change set into Elastic bulk API JSON form (so contains special new line
|
||||
* characters and bulk API header etc.
|
||||
*/
|
||||
public void writeJson(ChangeSet changeSet, Writer writer) throws IOException {
|
||||
|
||||
JsonGenerator generator = jsonFactory.createGenerator(writer);
|
||||
|
||||
List<BeanChange> changes = changeSet.getChanges();
|
||||
for (int i = 0; i < changes.size(); i++) {
|
||||
write(generator, changes.get(i), changeSet, i);
|
||||
}
|
||||
|
||||
generator.flush();
|
||||
generator.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the bean change as a single JSON document for storage into Elastic.
|
||||
* <p>
|
||||
* Note that for ease of search/use we effectively denormalise by including the transaction header
|
||||
* information in each bean document.
|
||||
* </p>
|
||||
*/
|
||||
protected void write(JsonGenerator gen, BeanChange bean, ChangeSet changeSet, int position) throws IOException {
|
||||
|
||||
writeBulkHeader(gen, changeSet, position);
|
||||
writeBeanChange(gen, bean, changeSet);
|
||||
writeBeanChangeEnd(gen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the bean change as JSON document containing the transaction header details.
|
||||
*/
|
||||
protected void writeBeanChange(JsonGenerator gen, BeanChange bean, ChangeSet changeSet) throws IOException {
|
||||
|
||||
gen.writeStartObject();
|
||||
|
||||
writeBeanTransactionDetails(gen, changeSet);
|
||||
|
||||
gen.writeStringField("object", bean.getTable());
|
||||
gen.writeStringField("objectId", bean.getId().toString());
|
||||
gen.writeStringField("change", bean.getType().getCode());
|
||||
gen.writeNumberField("eventTime", bean.getEventTime());
|
||||
|
||||
writeBeanValues(gen, bean);
|
||||
|
||||
gen.writeEndObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* For Elastic bulk we append raw new line character.
|
||||
*/
|
||||
protected void writeBeanChangeEnd(JsonGenerator gen) throws IOException {
|
||||
gen.writeRawValue("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Denormalise by writing the transaction header details.
|
||||
*/
|
||||
protected void writeBeanTransactionDetails(JsonGenerator gen, ChangeSet changeSet) throws IOException {
|
||||
|
||||
gen.writeStringField("txnId", changeSet.getTxnId());
|
||||
gen.writeStringField("txnState", changeSet.getTxnState().getCode());
|
||||
gen.writeNumberField("txnBatch", changeSet.getTxnBatch());
|
||||
gen.writeStringField("userId", changeSet.getUserId());
|
||||
String userIpAddress = changeSet.getUserIpAddress();
|
||||
if (userIpAddress != null) {
|
||||
gen.writeStringField("userIpAddress", userIpAddress);
|
||||
}
|
||||
String userContext = changeSet.getUserContext();
|
||||
if (userContext != null) {
|
||||
gen.writeStringField("userContext", userContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the elastic bulk API header.
|
||||
*/
|
||||
protected void writeBulkHeader(JsonGenerator gen, ChangeSet changeSet, int position) throws IOException {
|
||||
|
||||
// we index with an 'id' value so that we can process/reprocess the JSON and
|
||||
// avoid duplicates being inserted. Appending the batch and position give us
|
||||
// an effectively unique id value for the change
|
||||
String uid = changeSet.getTxnId() + "_" + changeSet.getTxnBatch() + "." + position;
|
||||
|
||||
// the 'header' for elastic bulk API
|
||||
gen.writeStartObject();
|
||||
gen.writeFieldName("index");
|
||||
gen.writeStartObject();
|
||||
gen.writeStringField("_index", indexName);
|
||||
gen.writeStringField("_type", indexType);
|
||||
gen.writeStringField("_id", uid);
|
||||
gen.writeEndObject();
|
||||
gen.writeEndObject();
|
||||
gen.writeRawValue("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* For insert and update write the new/old values.
|
||||
*/
|
||||
protected void writeBeanValues(JsonGenerator gen, BeanChange bean) throws IOException {
|
||||
if (bean.getType() != ChangeType.DELETE) {
|
||||
gen.writeFieldName("values");
|
||||
gen.writeStartObject();
|
||||
// use JsonScalar as it knows how to encode all the scalar
|
||||
// property types that Ebean supports (Java8, Joda etc)
|
||||
JsonScalar scalarWriter = json.getScalar(gen);
|
||||
writeValuePairs(bean, scalarWriter, gen);
|
||||
gen.writeEndObject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write all the value pairs suppressing null values.
|
||||
* <p>
|
||||
* We are intentionally keeping the same new/old structure for both inserts and updates.
|
||||
* </p>
|
||||
*/
|
||||
protected void writeValuePairs(BeanChange bean, JsonScalar scalarWriter, JsonGenerator gen) throws IOException {
|
||||
|
||||
for (Map.Entry<String, ValuePair> entry : bean.getValues().entrySet()) {
|
||||
gen.writeFieldName(entry.getKey());
|
||||
gen.writeStartObject();
|
||||
ValuePair value = entry.getValue();
|
||||
Object newValue = value.getNewValue();
|
||||
if (newValue != null) {
|
||||
scalarWriter.write("new", newValue);
|
||||
}
|
||||
Object oldValue = value.getOldValue();
|
||||
if (oldValue != null) {
|
||||
scalarWriter.write("old", oldValue);
|
||||
}
|
||||
gen.writeEndObject();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.avaje.ebeaninternal.server.changelog;
|
||||
|
||||
import com.avaje.ebean.event.changelog.ChangeLogPrepare;
|
||||
import com.avaje.ebean.event.changelog.ChangeSet;
|
||||
|
||||
/**
|
||||
* Placeholder/default implementation that does not do anything.
|
||||
* <p>
|
||||
* Generally an implementation should be provided that reads context
|
||||
* information such as user id and user ip address etc and sets that
|
||||
* on the changeSet.
|
||||
* </p>
|
||||
*/
|
||||
public class DefaultChangeLogPrepare implements ChangeLogPrepare {
|
||||
|
||||
/**
|
||||
* Just return true to send change set through to the logger.
|
||||
*/
|
||||
@Override
|
||||
public boolean prepare(ChangeSet changeSet) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.avaje.ebeaninternal.server.changelog;
|
||||
|
||||
import com.avaje.ebean.annotation.ChangeLog;
|
||||
import com.avaje.ebean.event.BeanPersistRequest;
|
||||
import com.avaje.ebean.event.changelog.ChangeLogFilter;
|
||||
import com.avaje.ebean.event.changelog.ChangeLogRegister;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Default implementation of ChangeLogRegister.
|
||||
*/
|
||||
public class DefaultChangeLogRegister implements ChangeLogRegister {
|
||||
|
||||
private static final BasicFilter INCLUDE_INSERTS = new BasicFilter(true);
|
||||
|
||||
private static final BasicFilter EXCLUDE_INSERTS = new BasicFilter(false);
|
||||
|
||||
|
||||
@Override
|
||||
public ChangeLogFilter getChangeFilter(Class<?> beanType) {
|
||||
|
||||
ChangeLog changeLog = beanType.getAnnotation(ChangeLog.class);
|
||||
if (changeLog == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String[] updatesThatInclude = changeLog.updatesThatInclude();
|
||||
if (updatesThatInclude.length == 0) {
|
||||
return changeLog.excludeInserts() ? EXCLUDE_INSERTS : INCLUDE_INSERTS;
|
||||
}
|
||||
|
||||
Set<String> updateProps = new HashSet<String>();
|
||||
for (int i = 0; i < updatesThatInclude.length; i++) {
|
||||
updateProps.add(updatesThatInclude[i]);
|
||||
}
|
||||
|
||||
return new UpdateFilter(!changeLog.excludeInserts(), updateProps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic filter that only handles include inserts flag.
|
||||
*/
|
||||
protected static class BasicFilter implements ChangeLogFilter {
|
||||
|
||||
final boolean includeInserts;
|
||||
|
||||
BasicFilter(boolean includeInserts) {
|
||||
this.includeInserts = includeInserts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean includeInsert(BeanPersistRequest<?> insertRequest) {
|
||||
return includeInserts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean includeUpdate(BeanPersistRequest<?> updateRequest) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean includeDelete(BeanPersistRequest<?> deleteRequest) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter that takes into account a set of properties to check for updates
|
||||
* as well as the include inserts flag.
|
||||
*/
|
||||
protected static class UpdateFilter extends BasicFilter {
|
||||
|
||||
final Set<String> updateProperties;
|
||||
|
||||
UpdateFilter(boolean includeInserts, Set<String> updateProperties) {
|
||||
super(includeInserts);
|
||||
this.updateProperties = updateProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean includeUpdate(BeanPersistRequest<?> updateRequest) {
|
||||
return updateRequest.hasDirtyProperty(updateProperties);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.avaje.ebeaninternal.server.changelog;
|
||||
|
||||
import com.avaje.ebean.event.changelog.ChangeLogListener;
|
||||
import com.avaje.ebean.event.changelog.ChangeSet;
|
||||
import com.avaje.ebean.plugin.SpiServer;
|
||||
import com.avaje.ebean.plugin.SpiServerPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
|
||||
/**
|
||||
* Logs the change sets in a Elastic Search Bulk API JSON format.
|
||||
*/
|
||||
public class ElasticChangeLogListener implements ChangeLogListener, SpiServerPlugin {
|
||||
|
||||
protected static final Logger appLog = LoggerFactory.getLogger(ElasticChangeLogListener.class);
|
||||
|
||||
protected static final Logger fileLog = LoggerFactory.getLogger("org.avaje.ebean.ElasticChangeLog");
|
||||
|
||||
protected BulkJsonBuilder jsonBuilder;
|
||||
|
||||
public ElasticChangeLogListener() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the underlying JSON handler.
|
||||
*/
|
||||
@Override
|
||||
public void configure(SpiServer server) {
|
||||
jsonBuilder = new BulkJsonBuilder(server.json(), "changelog", "changelog");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void online(boolean online) {
|
||||
// We don't care online or offline in this case
|
||||
// we might if we setup for network sending etc
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(ChangeSet changeSet) {
|
||||
|
||||
try {
|
||||
// I'm pretty sure I'm going to change this to use a FileWriter (but apache kafta could be good here too)
|
||||
// This buffer could get really big and to me normal logging (without a Writer) is not that well suited to
|
||||
// this problem so ... works but lets do better here and write direct to files (without the buffer issue)
|
||||
StringWriter writer = new StringWriter(getBufferSize(changeSet));
|
||||
jsonBuilder.writeJson(changeSet, writer);
|
||||
String json = writer.toString();
|
||||
|
||||
fileLog.info("Sending txnId:{} txnState:{} txnBatch:{} \n {}", changeSet.getTxnId(), changeSet.getTxnState(), changeSet.getTxnBatch(), json);
|
||||
|
||||
} catch (IOException e) {
|
||||
String msg = extractErrorMessage(e);
|
||||
fileLog.error("Exception sending txnId:{} txnState:{} txnBatch:{} error:{}", changeSet.getTxnId(), changeSet.getTxnState(), changeSet.getTxnBatch(), msg);
|
||||
appLog.error("Exception sending changeSet "+changeSet.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
protected int getBufferSize(ChangeSet changeSet) {
|
||||
// a rough guess, could get smarter here
|
||||
return Math.min(400 * changeSet.size(), 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an error message that does not have new line characters and hence safe to go into
|
||||
* our log which contains the payloads (that we want to be able to extract easily later on).
|
||||
*/
|
||||
@NotNull
|
||||
protected String extractErrorMessage(Exception e) {
|
||||
|
||||
String msg = e.toString();
|
||||
msg = msg.replace('\r','|');
|
||||
msg = msg.replace('\n', '|');
|
||||
return msg;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import com.avaje.ebean.event.BeanQueryAdapter;
|
||||
import com.avaje.ebean.meta.MetaInfoManager;
|
||||
import com.avaje.ebean.plugin.SpiBeanType;
|
||||
import com.avaje.ebean.plugin.SpiServer;
|
||||
import com.avaje.ebean.plugin.SpiServerPlugin;
|
||||
import com.avaje.ebean.text.csv.CsvReader;
|
||||
import com.avaje.ebean.text.json.JsonContext;
|
||||
import com.avaje.ebeaninternal.api.LoadBeanRequest;
|
||||
@@ -135,6 +136,8 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
|
||||
private final CQueryEngine cqueryEngine;
|
||||
|
||||
private final List<SpiServerPlugin> serverPlugins;
|
||||
|
||||
private DdlGenerator ddlGenerator;
|
||||
|
||||
private final ExpressionFactory expressionFactory;
|
||||
@@ -237,14 +240,24 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
|
||||
this.beanLoader = new DefaultBeanLoader(this);
|
||||
this.jsonContext = config.createJsonContext(this);
|
||||
this.serverPlugins = config.getPlugins();
|
||||
|
||||
// load normal plugins late and call setup on all
|
||||
loadAndInitializePlugins(config.getServerConfig());
|
||||
|
||||
configureServerPlugins();
|
||||
|
||||
// Register with the JVM Shutdown hook
|
||||
ShutdownManager.registerEbeanServer(this);
|
||||
}
|
||||
|
||||
private void configureServerPlugins() {
|
||||
|
||||
for (SpiServerPlugin plugin : serverPlugins) {
|
||||
plugin.configure(this);
|
||||
}
|
||||
}
|
||||
|
||||
protected void loadAndInitializePlugins(ServerConfig config) {
|
||||
|
||||
List<SpiEbeanPlugin> spiPlugins = new ArrayList<SpiEbeanPlugin>();
|
||||
@@ -283,6 +296,9 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
for (SpiEbeanPlugin plugin : ebeanPlugins) {
|
||||
plugin.execute(online);
|
||||
}
|
||||
for (SpiServerPlugin plugin : serverPlugins) {
|
||||
plugin.online(online);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
package com.avaje.ebeaninternal.server.core;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import com.avaje.ebean.config.dbplatform.DbHistorySupport;
|
||||
import com.avaje.ebeaninternal.server.deploy.generatedproperty.GeneratedPropertyFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.avaje.ebean.ExpressionFactory;
|
||||
import com.avaje.ebean.cache.ServerCacheManager;
|
||||
import com.avaje.ebean.config.ExternalTransactionManager;
|
||||
import com.avaje.ebean.config.ServerConfig;
|
||||
import com.avaje.ebean.config.dbplatform.DatabasePlatform;
|
||||
import com.avaje.ebean.config.dbplatform.DbHistorySupport;
|
||||
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.plugin.SpiServerPlugin;
|
||||
import com.avaje.ebean.text.json.JsonContext;
|
||||
import com.avaje.ebeaninternal.api.SpiBackgroundExecutor;
|
||||
import com.avaje.ebeaninternal.api.SpiEbeanServer;
|
||||
import com.avaje.ebeaninternal.server.autofetch.AutoFetchManager;
|
||||
import com.avaje.ebeaninternal.server.autofetch.AutoFetchManagerFactory;
|
||||
import com.avaje.ebeaninternal.server.changelog.DefaultChangeLogPrepare;
|
||||
import com.avaje.ebeaninternal.server.changelog.DefaultChangeLogRegister;
|
||||
import com.avaje.ebeaninternal.server.changelog.ElasticChangeLogListener;
|
||||
import com.avaje.ebeaninternal.server.cluster.ClusterManager;
|
||||
import com.avaje.ebeaninternal.server.deploy.BeanDescriptorManager;
|
||||
import com.avaje.ebeaninternal.server.deploy.DeployOrmXml;
|
||||
import com.avaje.ebeaninternal.server.deploy.generatedproperty.GeneratedPropertyFactory;
|
||||
import com.avaje.ebeaninternal.server.deploy.parse.DeployCreateProperties;
|
||||
import com.avaje.ebeaninternal.server.deploy.parse.DeployInherit;
|
||||
import com.avaje.ebeaninternal.server.deploy.parse.DeployUtil;
|
||||
@@ -42,7 +44,12 @@ import com.avaje.ebeaninternal.server.transaction.TransactionScopeManager;
|
||||
import com.avaje.ebeaninternal.server.type.DefaultTypeManager;
|
||||
import com.avaje.ebeaninternal.server.type.TypeManager;
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -93,6 +100,11 @@ public class InternalConfiguration {
|
||||
|
||||
private final JsonFactory jsonFactory;
|
||||
|
||||
/**
|
||||
* List of plugins (that ultimately the DefaultServer configures late in construction).
|
||||
*/
|
||||
private final List<SpiServerPlugin> plugins = new ArrayList<SpiServerPlugin>();
|
||||
|
||||
public InternalConfiguration(XmlConfig xmlConfig, ClusterManager clusterManager,
|
||||
ServerCacheManager cacheManager, SpiBackgroundExecutor backgroundExecutor,
|
||||
ServerConfig serverConfig, BootupClasses bootupClasses) {
|
||||
@@ -139,6 +151,45 @@ public class InternalConfiguration {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a SpiServerPlugin and if so 'collect' it to give the complete list
|
||||
* later on the DefaultServer for late call to configure().
|
||||
*/
|
||||
public <T> T plugin(T maybePlugin) {
|
||||
if (maybePlugin instanceof SpiServerPlugin) {
|
||||
plugins.add((SpiServerPlugin)maybePlugin);
|
||||
}
|
||||
return maybePlugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of plugins we collected during construction.
|
||||
*/
|
||||
public List<SpiServerPlugin> getPlugins() {
|
||||
return plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ChangeLogPrepare to use with a default implementation if none defined.
|
||||
*/
|
||||
public ChangeLogPrepare changeLogPrepare(ChangeLogPrepare prepare) {
|
||||
return plugin((prepare != null) ? prepare : new DefaultChangeLogPrepare());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ChangeLogRegister to use with a default implementation if none defined.
|
||||
*/
|
||||
public ChangeLogRegister changeLogRegister(ChangeLogRegister register) {
|
||||
return plugin((register != null) ? register : new DefaultChangeLogRegister());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ChangeLogListener to use with a default implementation if none defined.
|
||||
*/
|
||||
public ChangeLogListener changeLogListener(ChangeLogListener listener) {
|
||||
return plugin((listener != null) ? listener : new ElasticChangeLogListener());
|
||||
}
|
||||
|
||||
/**
|
||||
* For 'As Of' queries return the number of bind variables per predicate.
|
||||
*/
|
||||
|
||||
@@ -215,6 +215,14 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
|
||||
return intercept.getDirtyPropertyNames();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if any of the given property names are dirty.
|
||||
*/
|
||||
@Override
|
||||
public boolean hasDirtyProperty(Set<String> propertyNames) {
|
||||
return intercept.hasDirtyProperty(propertyNames);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, ValuePair> getUpdatedValues() {
|
||||
return intercept.getDirtyValues();
|
||||
|
||||
@@ -209,9 +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();
|
||||
this.changeLogPrepare = config.changeLogPrepare(bootupClasses.getChangeLogPrepare());
|
||||
this.changeLogListener = config.changeLogListener(bootupClasses.getChangeLogListener());
|
||||
this.changeLogRegister = config.changeLogRegister(bootupClasses.getChangeLogRegister());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.avaje.ebean.bean;
|
||||
|
||||
import com.avaje.ebean.BaseTestCase;
|
||||
import com.avaje.ebean.Ebean;
|
||||
import com.avaje.tests.model.basic.Customer;
|
||||
import com.avaje.tests.model.basic.ResetBasicData;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.sql.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class EntityBeanInterceptTest extends BaseTestCase {
|
||||
|
||||
@Test
|
||||
public void testHasDirtyProperty() throws Exception {
|
||||
|
||||
|
||||
ResetBasicData.reset();
|
||||
|
||||
List<Customer> list = Ebean.find(Customer.class).findList();
|
||||
|
||||
Set<String> propertyNames = new HashSet<String>();
|
||||
propertyNames.add("name");
|
||||
propertyNames.add("status");
|
||||
|
||||
|
||||
Customer customer = list.get(0);
|
||||
EntityBeanIntercept ebi = ((EntityBean)customer)._ebean_getIntercept();
|
||||
|
||||
assertFalse(ebi.hasDirtyProperty(propertyNames));
|
||||
|
||||
customer.setAnniversary(new Date(System.currentTimeMillis()));
|
||||
assertFalse(ebi.hasDirtyProperty(propertyNames));
|
||||
|
||||
customer.setStatus(Customer.Status.ACTIVE);
|
||||
assertTrue(ebi.hasDirtyProperty(propertyNames));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ public class BeanFindControllerTest extends BaseTestCase {
|
||||
|
||||
ServerConfig config = new ServerConfig();
|
||||
|
||||
config.setName("h2other");
|
||||
config.setName("h2otherfind");
|
||||
config.loadFromProperties();
|
||||
config.setDdlGenerate(true);
|
||||
config.setDdlRun(true);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.avaje.ebeaninternal.server.changelog;
|
||||
|
||||
import com.avaje.ebean.BaseTestCase;
|
||||
import com.avaje.ebean.Ebean;
|
||||
import com.avaje.ebean.text.json.JsonContext;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.StringWriter;
|
||||
|
||||
|
||||
public class BulkJsonBuilderTest extends BaseTestCase {
|
||||
|
||||
Helper helper = new Helper();
|
||||
|
||||
@Test
|
||||
public void testToJson() throws Exception {
|
||||
|
||||
JsonContext jsonContext = Ebean.getDefaultServer().json();
|
||||
BulkJsonBuilder builder = new BulkJsonBuilder(jsonContext, "changelog2", "changelog2");
|
||||
|
||||
|
||||
StringWriter buffer = new StringWriter();
|
||||
builder.writeJson(helper.createChangeSet("ABCD", 10), buffer);
|
||||
System.out.println(buffer.toString());
|
||||
|
||||
buffer = new StringWriter();
|
||||
builder.writeJson(helper.createChangeSet("ABCD-2", 15), buffer);
|
||||
System.out.println(buffer.toString());
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.avaje.ebeaninternal.server.changelog;
|
||||
|
||||
import com.avaje.ebean.BaseTestCase;
|
||||
import com.avaje.ebean.event.changelog.ChangeLogFilter;
|
||||
import com.avaje.tests.model.basic.Address;
|
||||
import com.avaje.tests.model.basic.Contact;
|
||||
import com.avaje.tests.model.basic.Country;
|
||||
import com.avaje.tests.model.basic.Customer;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class DefaultChangeLogRegisterTest extends BaseTestCase {
|
||||
|
||||
DefaultChangeLogRegister register = new DefaultChangeLogRegister();
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
|
||||
assertNull(register.getChangeFilter(Address.class));
|
||||
|
||||
ChangeLogFilter changeFilter = register.getChangeFilter(Customer.class);
|
||||
DefaultChangeLogRegister.UpdateFilter updateFilter = (DefaultChangeLogRegister.UpdateFilter)changeFilter;
|
||||
assertFalse(updateFilter.includeInserts);
|
||||
assertThat(updateFilter.updateProperties).containsExactly("name", "status");
|
||||
|
||||
changeFilter = register.getChangeFilter(Contact.class);
|
||||
DefaultChangeLogRegister.BasicFilter contactFilter = (DefaultChangeLogRegister.BasicFilter)changeFilter;
|
||||
assertTrue(contactFilter.includeInserts);
|
||||
|
||||
changeFilter = register.getChangeFilter(Country.class);
|
||||
DefaultChangeLogRegister.BasicFilter countryFilter = (DefaultChangeLogRegister.BasicFilter)changeFilter;
|
||||
assertFalse(countryFilter.includeInserts);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.avaje.ebeaninternal.server.changelog;
|
||||
|
||||
import com.avaje.ebean.BaseTestCase;
|
||||
import com.avaje.ebean.Ebean;
|
||||
import com.avaje.ebean.EbeanServer;
|
||||
import com.avaje.ebean.event.changelog.ChangeSet;
|
||||
import org.junit.Test;
|
||||
|
||||
public class ElasticChangeLogListenerTest extends BaseTestCase {
|
||||
|
||||
Helper helper = new Helper();
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
|
||||
ElasticChangeLogListener changeLogListener = new ElasticChangeLogListener();
|
||||
|
||||
EbeanServer defaultServer = Ebean.getDefaultServer();
|
||||
changeLogListener.configure(defaultServer.getPluginApi());
|
||||
|
||||
ChangeSet changeSet = helper.createChangeSet("INT-001", 13);
|
||||
|
||||
changeLogListener.log(changeSet);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.avaje.ebeaninternal.server.changelog;
|
||||
|
||||
import com.avaje.ebean.ValuePair;
|
||||
import com.avaje.ebean.event.changelog.BeanChange;
|
||||
import com.avaje.ebean.event.changelog.ChangeSet;
|
||||
import com.avaje.ebean.event.changelog.ChangeType;
|
||||
import com.avaje.ebean.event.changelog.TxnState;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class Helper {
|
||||
|
||||
public ChangeSet createChangeSet(String txnId, long startId) {
|
||||
|
||||
ChangeSet cs = new ChangeSet();
|
||||
cs.setTxnId(txnId);
|
||||
cs.setTxnState(TxnState.COMMITTED);
|
||||
cs.setTxnBatch(0);
|
||||
cs.setSource("myApp");
|
||||
cs.setUserId("user234");
|
||||
cs.setUserIpAddress("123.4.5.6");
|
||||
cs.setUserContext("user defined input");
|
||||
|
||||
List<BeanChange> changes = cs.getChanges();
|
||||
|
||||
changes.add(createInsert(startId));
|
||||
changes.add(createUpdate(startId));
|
||||
changes.add(createDelete(startId));
|
||||
|
||||
return cs;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private BeanChange createInsert(long startId) {
|
||||
Map<String, ValuePair> values = new LinkedHashMap<String, ValuePair>();
|
||||
values.put("name", new ValuePair("rob", null));
|
||||
values.put("modified", new ValuePair(new Timestamp(System.currentTimeMillis()), null));
|
||||
|
||||
BeanChange bean = new BeanChange("mytable", startId+1, ChangeType.INSERT, null);
|
||||
bean.setValues(values);
|
||||
return bean;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private BeanChange createUpdate(long startId) {
|
||||
Map<String, ValuePair> values = new LinkedHashMap<String, ValuePair>();
|
||||
values.put("name", new ValuePair("jim", "steve"));
|
||||
values.put("nowHasVal", new ValuePair("wasNull", null));
|
||||
values.put("nowNull", new ValuePair(null, "hadVal"));
|
||||
|
||||
values.put("modified", new ValuePair(new Timestamp(System.currentTimeMillis()), null));
|
||||
|
||||
BeanChange bean = new BeanChange("mytable", startId+2, ChangeType.UPDATE, null);
|
||||
bean.setValues(values);
|
||||
return bean;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private BeanChange createDelete(long startId) {
|
||||
return new BeanChange("mytable", startId+3, ChangeType.DELETE, new HashMap<String, ValuePair>());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,8 +10,10 @@ import javax.persistence.OneToMany;
|
||||
import javax.persistence.Version;
|
||||
|
||||
import com.avaje.ebean.annotation.CacheStrategy;
|
||||
import com.avaje.ebean.annotation.ChangeLog;
|
||||
import com.avaje.ebean.annotation.CreatedTimestamp;
|
||||
|
||||
@ChangeLog
|
||||
@Entity
|
||||
@CacheStrategy(naturalKey="email")
|
||||
public class Contact {
|
||||
|
||||
@@ -7,10 +7,12 @@ import javax.validation.constraints.Size;
|
||||
|
||||
import com.avaje.ebean.annotation.CacheStrategy;
|
||||
import com.avaje.ebean.annotation.CacheTuning;
|
||||
import com.avaje.ebean.annotation.ChangeLog;
|
||||
|
||||
/**
|
||||
* Country entity bean.
|
||||
*/
|
||||
@ChangeLog(excludeInserts = true)
|
||||
@CacheStrategy(readOnly=true,warmingQuery="order by name")
|
||||
@CacheTuning(maxSize=500)
|
||||
@Entity
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.avaje.tests.model.basic;
|
||||
|
||||
import com.avaje.ebean.annotation.ChangeLog;
|
||||
import com.avaje.ebean.annotation.EnumValue;
|
||||
import com.avaje.ebean.annotation.JsonIgnore;
|
||||
import com.avaje.ebean.annotation.Where;
|
||||
@@ -20,6 +21,7 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
/**
|
||||
* Customer entity bean.
|
||||
*/
|
||||
@ChangeLog(excludeInserts = true, updatesThatInclude = {"name","status"})
|
||||
@Entity
|
||||
@Table(name = "o_customer")
|
||||
public class Customer extends BasicDomain {
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.sql.Timestamp;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@ChangeLog
|
||||
@ChangeLog(updatesThatInclude = {"name","shortDescription"})
|
||||
@Entity
|
||||
public class EBasicChangeLog {
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import javax.persistence.Version;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.xml.bind.annotation.XmlType;
|
||||
|
||||
import com.avaje.ebean.annotation.ChangeLog;
|
||||
import com.avaje.ebean.annotation.Formula;
|
||||
import com.avaje.ebean.annotation.WhenCreated;
|
||||
import com.avaje.ebean.annotation.Where;
|
||||
@@ -29,6 +30,7 @@ import com.avaje.ebean.annotation.Where;
|
||||
/**
|
||||
* Order entity bean.
|
||||
*/
|
||||
@ChangeLog
|
||||
@Entity
|
||||
@Table(name = "o_order")
|
||||
public class Order implements Serializable {
|
||||
|
||||
@@ -80,6 +80,11 @@ datasource.h2other.password=
|
||||
datasource.h2other.databaseUrl=jdbc:h2:mem:h2other;DB_CLOSE_DELAY=-1
|
||||
datasource.h2other.databaseDriver=org.h2.Driver
|
||||
|
||||
datasource.h2otherfind.username=sa
|
||||
datasource.h2otherfind.password=
|
||||
datasource.h2otherfind.databaseUrl=jdbc:h2:mem:h2otherfind;DB_CLOSE_DELAY=-1
|
||||
datasource.h2otherfind.databaseDriver=org.h2.Driver
|
||||
|
||||
datasource.h2ebasicver.username=sa
|
||||
datasource.h2ebasicver.password=
|
||||
datasource.h2ebasicver.databaseUrl=jdbc:h2:mem:h2ebasicver;DB_CLOSE_DELAY=-1
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</appender>
|
||||
|
||||
<appender name="APPLICATION" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<File>log/ebean.log</File>
|
||||
<File>log/application.log</File>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>TRACE</level>
|
||||
</filter>
|
||||
@@ -22,6 +22,21 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="ELASTIC_CHANGE_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<File>log/changeLog.log</File>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<FileNamePattern>log/changeLog.log.%d{yyyy-MM-dd}</FileNamePattern>
|
||||
<MaxHistory>90</MaxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||
<pattern>-- %d{HH:mm:ss.SSS} %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="org.avaje.ebean.ElasticChangeLog" level="TRACE" additivity="false">
|
||||
<appender-ref ref="ELASTIC_CHANGE_LOG" />
|
||||
</logger>
|
||||
|
||||
<root level="WARN">
|
||||
<appender-ref ref="APPLICATION"/>
|
||||
<appender-ref ref="STDOUT"/>
|
||||
|
||||
Reference in New Issue
Block a user