diff --git a/src/main/java/com/avaje/ebean/Query.java b/src/main/java/com/avaje/ebean/Query.java index ce1582255..ce8ce3b1c 100644 --- a/src/main/java/com/avaje/ebean/Query.java +++ b/src/main/java/com/avaje/ebean/Query.java @@ -366,6 +366,16 @@ public interface Query extends Serializable { */ Query setLazyLoadBatchSize(int lazyLoadBatchSize); + /** + * Disable read auditing for this query. + *

+ * This is intended to be used when the query is not a user initiated query and instead + * part of the internal processing in an application to load a cache or document store etc. + * In these cases we don't want the query to be part of read auditing. + *

+ */ + Query setDisableReadAuditing(); + /** * Explicitly set a comma delimited list of the properties to fetch on the * 'main' root level entity bean (aka partial object). Note that '*' means all diff --git a/src/main/java/com/avaje/ebean/annotation/ReadAudit.java b/src/main/java/com/avaje/ebean/annotation/ReadAudit.java new file mode 100644 index 000000000..33d51159f --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/ReadAudit.java @@ -0,0 +1,15 @@ +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 read auditing. + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ReadAudit { + +} diff --git a/src/main/java/com/avaje/ebean/config/ServerConfig.java b/src/main/java/com/avaje/ebean/config/ServerConfig.java index 1cdc2290c..1054e2acd 100644 --- a/src/main/java/com/avaje/ebean/config/ServerConfig.java +++ b/src/main/java/com/avaje/ebean/config/ServerConfig.java @@ -11,6 +11,8 @@ 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.event.readaudit.ReadAuditLogger; +import com.avaje.ebean.event.readaudit.ReadAuditPrepare; import com.avaje.ebean.meta.MetaInfoManager; import com.avaje.ebean.util.ClassUtil; import com.fasterxml.jackson.core.JsonFactory; @@ -299,6 +301,10 @@ public class ServerConfig { private ChangeLogRegister changeLogRegister; + private ReadAuditLogger readAuditLogger; + + private ReadAuditPrepare readAuditPrepare; + private EncryptKeyManager encryptKeyManager; private EncryptDeployManager encryptDeployManager; @@ -725,6 +731,41 @@ public class ServerConfig { this.changeLogIncludeInserts = changeLogIncludeInserts; } + /** + * Return the ReadAuditLogger to use. + */ + public ReadAuditLogger getReadAuditLogger() { + return readAuditLogger; + } + + /** + * Set the ReadAuditLogger to use. If not set the default implementation is used + * which logs the read events in JSON format to a standard named SLF4J logger + * (which can be configured in say logback to log to a separate log file). + */ + public void setReadAuditLogger(ReadAuditLogger readAuditLogger) { + this.readAuditLogger = readAuditLogger; + } + + /** + * Return the ReadAuditPrepare to use. + */ + public ReadAuditPrepare getReadAuditPrepare() { + return readAuditPrepare; + } + + /** + * Set the ReadAuditPrepare to use. + *

+ * It is expected that an implementation is used that read user context information + * (user id, user ip address etc) and sets it on the ReadEvent bean before it is sent + * to the ReadAuditLogger. + *

+ */ + public void setReadAuditPrepare(ReadAuditPrepare readAuditPrepare) { + this.readAuditPrepare = readAuditPrepare; + } + /** * Return the DB migration configuration. */ diff --git a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/DbTriggerBasedHistoryDdl.java b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/DbTriggerBasedHistoryDdl.java index 47e400ad7..73cf0f8c5 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/DbTriggerBasedHistoryDdl.java +++ b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/DbTriggerBasedHistoryDdl.java @@ -176,7 +176,7 @@ public abstract class DbTriggerBasedHistoryDdl implements PlatformHistoryDdl { String platformType = platformDdl.convert(type, false); buffer.append(" "); - buffer.append(platformDdl.lowerName(columnName), 30); + buffer.append(platformDdl.lowerName(columnName), 29); buffer.append(platformType); } diff --git a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/PlatformDdl.java b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/PlatformDdl.java index 3e0e5caf8..d5d5520bc 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/PlatformDdl.java +++ b/src/main/java/com/avaje/ebean/dbmigration/ddlgeneration/platform/PlatformDdl.java @@ -136,7 +136,7 @@ public class PlatformDdl { String platformType = convert(column.getType(), identityColumn); buffer.append(" "); - buffer.append(lowerName(column.getName()), 30); + buffer.append(lowerName(column.getName()), 29); buffer.append(platformType); if (isTrue(column.isNotnull()) || isTrue(column.isPrimaryKey())) { buffer.append(" not null"); diff --git a/src/main/java/com/avaje/ebean/event/changelog/BeanChange.java b/src/main/java/com/avaje/ebean/event/changelog/BeanChange.java index 5e69cacb7..225e04010 100644 --- a/src/main/java/com/avaje/ebean/event/changelog/BeanChange.java +++ b/src/main/java/com/avaje/ebean/event/changelog/BeanChange.java @@ -51,6 +51,10 @@ public class BeanChange { public BeanChange() { } + public String toString() { + return "table:" + table + " id:" + id+" values:"+values; + } + /** * Return the object type (typically table name). */ diff --git a/src/main/java/com/avaje/ebean/event/readaudit/ReadAuditLogger.java b/src/main/java/com/avaje/ebean/event/readaudit/ReadAuditLogger.java new file mode 100644 index 000000000..8e18abe4e --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/readaudit/ReadAuditLogger.java @@ -0,0 +1,37 @@ +package com.avaje.ebean.event.readaudit; + +/** + * Log that the query was executed + */ +public interface ReadAuditLogger { + + /** + * Called when a new query plan is created. + *

+ * The query plan has the full sql and logging the query plan separately means that each of + * the bean and many read events can log the query plan key and not the full sql (reducing the + * bulk size of the read audit logs). + *

+ */ + void queryPlan(ReadAuditQueryPlan queryPlan); + + /** + * Audit a find bean query that returned a bean. + *

+ * Finds that did not return a bean are excluded. + *

+ */ + void auditBean(ReadEvent readBean); + + /** + * Audit a find many query that returned some beans. + *

+ * Finds that did not return any beans are excluded. + *

+ *

+ * For large queries executed via findEach() etc the ids are collected in batches + * and logged. Hence the ids list has a maximum size of the batch size. + *

+ */ + void auditMany(ReadEvent readMany); +} diff --git a/src/main/java/com/avaje/ebean/event/readaudit/ReadAuditPrepare.java b/src/main/java/com/avaje/ebean/event/readaudit/ReadAuditPrepare.java new file mode 100644 index 000000000..fb80d23af --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/readaudit/ReadAuditPrepare.java @@ -0,0 +1,23 @@ +package com.avaje.ebean.event.readaudit; + +/** + * Set user context information into the read event prior to it being logged. + */ +public interface ReadAuditPrepare { + + /** + * Prepare the read event by setting any user context information into the read event such as the + * application user id and ip address. + *

+ * This method is called prior to the read event being sent to the ReadAuditLogger. + *

+ *

+ * Note that for findFutureList() queries prepare() is called early in the foreground thread + * prior to the query executing and at that point the ReadEvent bean only has the bean type + * and no other details (which are populated later when the query is executed in the background + * thread). + *

+ */ + void prepare(ReadEvent readEvent); + +} diff --git a/src/main/java/com/avaje/ebean/event/readaudit/ReadAuditQueryPlan.java b/src/main/java/com/avaje/ebean/event/readaudit/ReadAuditQueryPlan.java new file mode 100644 index 000000000..8d256de31 --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/readaudit/ReadAuditQueryPlan.java @@ -0,0 +1,77 @@ +package com.avaje.ebean.event.readaudit; + +/** + * A SQL query and associated keys. + *

+ * This is logged as a separate event so that the + *

+ */ +public class ReadAuditQueryPlan { + + String beanType; + + String queryKey; + + String sql; + + /** + * Construct given the beanType, queryKey and sql. + */ + public ReadAuditQueryPlan(String beanType, String queryKey, String sql) { + this.beanType = beanType; + this.queryKey = queryKey; + this.sql = sql; + } + + /** + * Construct for JSON tools. + */ + public ReadAuditQueryPlan() { + } + + public String toString() { + return "beanType:" + beanType + " queryKey:" + queryKey + " sql:" + sql; + } + + /** + * Return the bean type. + */ + public String getBeanType() { + return beanType; + } + + /** + * Set the bean type. + */ + public void setBeanType(String beanType) { + this.beanType = beanType; + } + + /** + * Return the query key (relative to the bean type). + */ + public String getQueryKey() { + return queryKey; + } + + /** + * Set the query key. + */ + public void setQueryKey(String queryKey) { + this.queryKey = queryKey; + } + + /** + * Return the sql statement. + */ + public String getSql() { + return sql; + } + + /** + * Set the sql statement. + */ + public void setSql(String sql) { + this.sql = sql; + } +} diff --git a/src/main/java/com/avaje/ebean/event/readaudit/ReadEvent.java b/src/main/java/com/avaje/ebean/event/readaudit/ReadEvent.java new file mode 100644 index 000000000..904742c95 --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/readaudit/ReadEvent.java @@ -0,0 +1,261 @@ +package com.avaje.ebean.event.readaudit; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Read event sent to the ReadEventLogger. + *

+ * This is a flattened in that it contains either a read bean or list of beans. It is flattened + * in this way to simplify logging and processing and simply means that it either contains an + * id or a list of ids. + *

+ */ +public class ReadEvent { + + /** + * User defined 'source' such as the application name. + */ + protected String source; + + /** + * Application user id expected to be optionally populated by ChangeLogPrepare. + */ + protected String userId; + + /** + * Application user ip address expected to be optionally populated by ChangeLogPrepare. + */ + protected String userIpAddress; + + /** + * Arbitrary user context information expected to be optionally populated by ChangeLogPrepare. + */ + protected Map userContext; + + /** + * The time the bean change was created. + */ + protected long eventTime; + + /** + * The type of the bean(s) read. + */ + protected String beanType; + + /** + * The query key (relative to the bean type). + */ + protected String queryKey; + + /** + * The bind log when the query was executed. + */ + protected String bindLog; + + /** + * The id of the bean read. + */ + protected Object id; + + /** + * The ids of the beans read. + */ + protected List ids; + + /** + * Common constructor for single bean and multi-bean read events. + */ + protected ReadEvent(String beanType, String queryKey, String bindLog) { + this.beanType = beanType; + this.queryKey = queryKey; + this.bindLog = bindLog; + this.eventTime = System.currentTimeMillis(); + } + + /** + * Construct for a single bean read. + */ + public ReadEvent(String beanType, String queryKey, String bindLog, Object id) { + this(beanType, queryKey, bindLog); + this.id = id; + } + + /** + * Construct for many beans read. + */ + public ReadEvent(String beanType, String queryKey, String bindLog, List ids) { + this(beanType, queryKey, bindLog); + this.ids = ids; + } + + /** + * Construct for many future list query. + */ + public ReadEvent(String beanType) { + this.beanType = beanType; + this.eventTime = System.currentTimeMillis(); + } + + /** + * Constructor for JSON tools. + */ + public ReadEvent() { + } + + /** + * 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 Map getUserContext() { + if (userContext == null) { + userContext = new LinkedHashMap(); + } + 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(Map userContext) { + this.userContext = userContext; + } + + /** + * Return the type of bean read. + */ + public String getBeanType() { + return beanType; + } + + /** + * Set the type of bean read. + */ + public void setBeanType(String beanType) { + this.beanType = beanType; + } + + /** + * Return the query key (relative to the bean type). + */ + public String getQueryKey() { + return queryKey; + } + + /** + * Set the query key (relative to the bean type). + */ + public void setQueryKey(String queryKey) { + this.queryKey = queryKey; + } + + /** + * Return the bind log used when executing the query. + */ + public String getBindLog() { + return bindLog; + } + + /** + * Set the bind log used when executing the query. + */ + public void setBindLog(String bindLog) { + this.bindLog = bindLog; + } + + /** + * Return the event date time. + */ + public long getEventTime() { + return eventTime; + } + + /** + * Set the event date time. + */ + public void setEventTime(long eventTime) { + this.eventTime = eventTime; + } + + /** + * Return the id of the bean read. + */ + public Object getId() { + return id; + } + + /** + * Set the id of the bean read. + */ + public void setId(Object id) { + this.id = id; + } + + /** + * Return the ids of the beans read. + */ + public List getIds() { + return ids; + } + + /** + * Set the ids of the beans read. + */ + public void setIds(List ids) { + this.ids = ids; + } +} diff --git a/src/main/java/com/avaje/ebean/text/json/EJson.java b/src/main/java/com/avaje/ebean/text/json/EJson.java index 947dbbf1e..bc38e30d4 100644 --- a/src/main/java/com/avaje/ebean/text/json/EJson.java +++ b/src/main/java/com/avaje/ebean/text/json/EJson.java @@ -3,6 +3,7 @@ package com.avaje.ebean.text.json; import java.io.IOException; import java.io.Reader; import java.io.Writer; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -36,6 +37,13 @@ public class EJson { EJsonWriter.write(object, jsonGenerator); } + /** + * Write the collection as json array to the jsonGenerator. + */ + public static void writeCollection(Collection collection, JsonGenerator jsonGenerator) throws IOException { + EJsonWriter.writeCollection(collection, jsonGenerator); + } + /** * Parse the json and return as a Map additionally specifying if the returned map should * be modify aware meaning that it can detect when it has been modified. diff --git a/src/main/java/com/avaje/ebean/text/json/EJsonWriter.java b/src/main/java/com/avaje/ebean/text/json/EJsonWriter.java index 7be808cb6..0deda8742 100644 --- a/src/main/java/com/avaje/ebean/text/json/EJsonWriter.java +++ b/src/main/java/com/avaje/ebean/text/json/EJsonWriter.java @@ -37,6 +37,10 @@ class EJsonWriter { new EJsonWriter(jsonGenerator).writeJson(object); } + static void writeCollection(Collection collection, JsonGenerator jsonGenerator) throws IOException { + new EJsonWriter(jsonGenerator).writeCollection(null, collection); + } + private final JsonGenerator jsonGenerator; private EJsonWriter(JsonGenerator jsonGenerator) { @@ -53,6 +57,12 @@ class EJsonWriter { if (object == null) { writeNull(name); + } else if (object instanceof Number) { + writeNumber(name, (Number) object); + + } else if (object instanceof String) { + writeString(name, (String) object); + } else if (object instanceof Map) { writeMap(name, (Map) object); @@ -62,15 +72,9 @@ class EJsonWriter { } else if (object instanceof Boolean) { writeBoolean(name, (Boolean) object); - } else if (object instanceof Number) { - writeNumber(name, (Number) object); - } else if (object instanceof Date) { writeDate(name, (Date) object); - } else if (object instanceof String) { - writeString(name, (String) object); - } else if (object instanceof Map.Entry) { Map.Entry entry = (Map.Entry) object; writeJson(entry.getKey().toString(), entry.getValue()); diff --git a/src/main/java/com/avaje/ebeaninternal/api/HashQueryPlan.java b/src/main/java/com/avaje/ebeaninternal/api/HashQueryPlan.java index 83ab5ebdf..d4c75e05d 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/HashQueryPlan.java +++ b/src/main/java/com/avaje/ebeaninternal/api/HashQueryPlan.java @@ -20,7 +20,16 @@ public class HashQueryPlan { public String toString() { return planHash+":"+bindCount+(rawSql != null ? ":r" : ""); } - + + /** + * Return as a partial key. For rawSql hash the sql is part of the key and as such + * needs to be included in order to have a complete key. Typically the MD5 of the sql + * can be used as a shot form proxy for the actual sql. + */ + public String getPartialKey() { + return planHash+"_"+bindCount; + } + public int hashCode() { int hc = planHash; hc = hc * 31 + bindCount; diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiEbeanServer.java b/src/main/java/com/avaje/ebeaninternal/api/SpiEbeanServer.java index 290a6e4fa..f987355ec 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiEbeanServer.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiEbeanServer.java @@ -11,6 +11,8 @@ import com.avaje.ebean.bean.CallStack; import com.avaje.ebean.bean.ObjectGraphNode; import com.avaje.ebean.config.ServerConfig; import com.avaje.ebean.config.dbplatform.DatabasePlatform; +import com.avaje.ebean.event.readaudit.ReadAuditLogger; +import com.avaje.ebean.event.readaudit.ReadAuditPrepare; import com.avaje.ebeaninternal.server.autofetch.AutoFetchManager; import com.avaje.ebeaninternal.server.core.SpiOrmQueryRequest; import com.avaje.ebean.dbmigration.DdlGenerator; @@ -195,4 +197,14 @@ public interface SpiEbeanServer extends EbeanServer, BeanLoader, BeanCollectionL */ void collectQueryStats(ObjectGraphNode objectGraphNode, long loadedBeanCount, long timeMicros); + /** + * Return the ReadAuditLogger to use for logging all read audit events. + */ + ReadAuditLogger getReadAuditLogger(); + + /** + * Return the ReadAuditPrepare used to populate the read audit events with + * user context information (user id, user ip address etc). + */ + ReadAuditPrepare getReadAuditPrepare(); } diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java index 1f496d319..a1b52d7bc 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java @@ -10,6 +10,7 @@ import com.avaje.ebean.bean.CallStack; import com.avaje.ebean.bean.ObjectGraphNode; import com.avaje.ebean.bean.PersistenceContext; import com.avaje.ebean.event.BeanQueryRequest; +import com.avaje.ebean.event.readaudit.ReadEvent; import com.avaje.ebeaninternal.server.autofetch.AutoFetchManager; import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocMany; @@ -644,8 +645,13 @@ public interface SpiQuery extends Query { int getBufferFetchSizeHint(); /** - * Return true if this is a query executing in the background. + * Return true if read auditing is disabled on this query. */ + boolean isDisableReadAudit(); + + /** + * Return true if this is a query executing in the background. + */ boolean isFutureFetch(); /** @@ -654,6 +660,16 @@ public interface SpiQuery extends Query { */ void setFutureFetch(boolean futureFetch); + /** + * Set the readEvent for future queries (as prepared in foreground thread). + */ + void setFutureFetchAudit(ReadEvent event); + + /** + * Read the readEvent for future queries (null otherwise). + */ + ReadEvent getFutureFetchAudit(); + /** * Set the underlying cancelable query (with the PreparedStatement). */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/changelog/ChangeJsonBuilder.java b/src/main/java/com/avaje/ebeaninternal/server/changelog/ChangeJsonBuilder.java index 758db627d..d57339f7e 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/changelog/ChangeJsonBuilder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/changelog/ChangeJsonBuilder.java @@ -93,6 +93,7 @@ public class ChangeJsonBuilder { * 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(); diff --git a/src/main/java/com/avaje/ebeaninternal/server/changelog/DefaultChangeLogListener.java b/src/main/java/com/avaje/ebeaninternal/server/changelog/DefaultChangeLogListener.java index 2267d45ab..1271c5f6c 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/changelog/DefaultChangeLogListener.java +++ b/src/main/java/com/avaje/ebeaninternal/server/changelog/DefaultChangeLogListener.java @@ -9,7 +9,6 @@ import com.avaje.ebean.plugin.SpiServerPlugin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; import java.io.StringWriter; import java.util.List; import java.util.Properties; @@ -80,17 +79,17 @@ public class DefaultChangeLogListener implements ChangeLogListener, SpiServerPlu @Override public void log(ChangeSet changeSet) { - try { - List changes = changeSet.getChanges(); - for (int i = 0; i < changes.size(); i++) { - // log each bean change as a separate log entry - BeanChange beanChange = changes.get(i); + List changes = changeSet.getChanges(); + for (int i = 0; i < changes.size(); i++) { + // log each bean change as a separate log entry + BeanChange beanChange = changes.get(i); + try { StringWriter writer = new StringWriter(getBufferSize(beanChange)); jsonBuilder.writeBeanJson(writer, beanChange, changeSet, i); changeLog.info(writer.toString()); + } catch (Exception e) { + logger.error("Exception logging beanChange " + beanChange.toString(), e); } - } catch (IOException e) { - logger.error("Exception sending changeSet " + changeSet.toString(), e); } } 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 256f36f2e..7a1aa902a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/BootupClasses.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/BootupClasses.java @@ -13,6 +13,8 @@ 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.ebean.event.readaudit.ReadAuditLogger; +import com.avaje.ebean.event.readaudit.ReadAuditPrepare; import com.avaje.ebeaninternal.server.type.ScalarType; import com.avaje.ebeaninternal.server.util.ClassPathSearchMatcher; import org.slf4j.Logger; @@ -69,10 +71,14 @@ public class BootupClasses implements ClassPathSearchMatcher { private Class changeLogPrepareClass; private Class changeLogListenerClass; private Class changeLogRegisterClass; + private Class readAuditPrepareClass; + private Class readAuditLoggerClass; private ChangeLogPrepare changeLogPrepare; private ChangeLogListener changeLogListener; private ChangeLogRegister changeLogRegister; + private ReadAuditPrepare readAuditPrepare; + private ReadAuditLogger readAuditLogger; public BootupClasses() { } @@ -192,6 +198,17 @@ public class BootupClasses implements ClassPathSearchMatcher { } public void addChangeLogInstances(ServerConfig serverConfig) { + + readAuditPrepare = serverConfig.getReadAuditPrepare(); + readAuditLogger = serverConfig.getReadAuditLogger(); + + if (readAuditPrepare == null && readAuditPrepareClass != null) { + readAuditPrepare = (ReadAuditPrepare)create(readAuditPrepareClass, false); + } + if (readAuditLogger == null && readAuditLoggerClass != null) { + readAuditLogger = (ReadAuditLogger)create(readAuditLoggerClass, false); + } + changeLogListener = serverConfig.getChangeLogListener(); changeLogRegister = serverConfig.getChangeLogRegister(); changeLogPrepare = serverConfig.getChangeLogPrepare(); @@ -262,6 +279,14 @@ public class BootupClasses implements ClassPathSearchMatcher { return changeLogRegister; } + public ReadAuditPrepare getReadAuditPrepare() { + return readAuditPrepare; + } + + public ReadAuditLogger getReadAuditLogger() { + return readAuditLogger; + } + public List getBeanQueryAdapters() { // add class registered BeanQueryAdapter to the already created instances for (Class cls : beanQueryAdapterList) { @@ -440,6 +465,15 @@ public class BootupClasses implements ClassPathSearchMatcher { interesting = true; } + if (ReadAuditPrepare.class.isAssignableFrom(cls)) { + readAuditPrepareClass = cls; + interesting = true; + } + if (ReadAuditLogger.class.isAssignableFrom(cls)) { + readAuditLoggerClass = cls; + interesting = true; + } + return interesting; } 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 a716516c5..0aa536900 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java @@ -12,8 +12,11 @@ import com.avaje.ebean.cache.ServerCacheManager; import com.avaje.ebean.config.EncryptKeyManager; import com.avaje.ebean.config.ServerConfig; import com.avaje.ebean.config.dbplatform.DatabasePlatform; +import com.avaje.ebean.dbmigration.DdlGenerator; import com.avaje.ebean.event.BeanPersistController; import com.avaje.ebean.event.BeanQueryAdapter; +import com.avaje.ebean.event.readaudit.ReadAuditLogger; +import com.avaje.ebean.event.readaudit.ReadAuditPrepare; import com.avaje.ebean.meta.MetaInfoManager; import com.avaje.ebean.plugin.SpiBeanType; import com.avaje.ebean.plugin.SpiServer; @@ -34,7 +37,6 @@ import com.avaje.ebeaninternal.api.SpiSqlQuery; import com.avaje.ebeaninternal.api.SpiTransaction; import com.avaje.ebeaninternal.api.TransactionEventTable; import com.avaje.ebeaninternal.server.autofetch.AutoFetchManager; -import com.avaje.ebean.dbmigration.DdlGenerator; import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; import com.avaje.ebeaninternal.server.deploy.BeanDescriptorManager; import com.avaje.ebeaninternal.server.deploy.BeanProperty; @@ -134,6 +136,10 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { private final AutoFetchManager autoFetchManager; + private final ReadAuditPrepare readAuditPrepare; + + private final ReadAuditLogger readAuditLogger; + private final CQueryEngine cqueryEngine; private final List serverPlugins; @@ -237,6 +243,8 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { this.autoFetchManager = config.createAutoFetchManager(this); this.adminAutofetch = new MAdminAutofetch(autoFetchManager); + this.readAuditPrepare = config.getReadAuditPrepare(); + this.readAuditLogger = config.getReadAuditLogger(); this.beanLoader = new DefaultBeanLoader(this); this.jsonContext = config.createJsonContext(this); @@ -353,6 +361,16 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { return autoFetchManager; } + @Override + public ReadAuditPrepare getReadAuditPrepare() { + return readAuditPrepare; + } + + @Override + public ReadAuditLogger getReadAuditLogger() { + return readAuditLogger; + } + /** * Run any initialisation required before registering with the ClusterManager. */ @@ -1393,6 +1411,11 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { // FutureList query always run in it's own persistence content spiQuery.setPersistenceContext(new DefaultPersistenceContext()); + if (!spiQuery.isDisableReadAudit()) { + BeanDescriptor desc = beanDescriptorManager.getBeanDescriptor(spiQuery.getBeanType()); + desc.readAuditFutureList(spiQuery); + } + // Create a new transaction solely to execute the findList() at some future time Transaction newTxn = createTransaction(); CallableQueryList call = new CallableQueryList(this, spiQuery, newTxn); diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DiffHelp.java b/src/main/java/com/avaje/ebeaninternal/server/core/DiffHelp.java index 6e24fc251..85407c1cf 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DiffHelp.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DiffHelp.java @@ -60,19 +60,19 @@ public class DiffHelp { public void diff(String prefix, Map map, EntityBean newBean, EntityBean oldBean, 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); - Object oldVal = (oldBean == null) ? null : base[i].getValue(oldBean); - if (!ValueUtil.areEqual(newVal, oldVal)) { - String propName = (prefix == null) ? base[i].getName() : prefix + base[i].getName(); - map.put(propName, new ValuePair(newVal, oldVal)); - } - } + if (flatMode) { + desc.diff(prefix, map, newBean, oldBean); + } else { - diffAssocOne(prefix, newBean, oldBean, desc, map); - diffEmbedded(prefix, newBean, oldBean, desc, map); + // check the simple properties + BeanProperty[] base = desc.propertiesBaseScalar(); + for (int i = 0; i < base.length; i++) { + base[i].diff(prefix, map, newBean, oldBean); + } + + diffAssocOne(prefix, newBean, oldBean, desc, map); + diffEmbedded(prefix, newBean, oldBean, desc, map); + } } /** @@ -96,7 +96,7 @@ public class DiffHelp { // one of the embedded beans is null if (flatMode) { BeanDescriptor embDesc = emb[i].getTargetDescriptor(); - diff(emb[i].getName()+".", map, newVal, oldVal, embDesc); + diff(propName, map, newVal, oldVal, embDesc); } else { map.put(propName, new ValuePair(newVal, oldVal)); } @@ -104,7 +104,7 @@ public class DiffHelp { } else { // recursively diff into the embedded bean BeanDescriptor embDesc = emb[i].getTargetDescriptor(); - diff(emb[i].getName()+".", map, newVal, oldVal, embDesc); + diff(propName, map, newVal, oldVal, embDesc); } } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DiffHelpInsert.java b/src/main/java/com/avaje/ebeaninternal/server/core/DiffHelpInsert.java deleted file mode 100644 index f59f9f1c5..000000000 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DiffHelpInsert.java +++ /dev/null @@ -1,102 +0,0 @@ -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/DiffHelpUpdate.java b/src/main/java/com/avaje/ebeaninternal/server/core/DiffHelpUpdate.java new file mode 100644 index 000000000..60c5b0e50 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DiffHelpUpdate.java @@ -0,0 +1,62 @@ +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.BeanPropertyAssocMany; +import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocOne; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Flattens an existing diff map converting assoc one beans into the associated id changes. + */ +public class DiffHelpUpdate { + + public static Map flatten(Map values, BeanDescriptor desc) { + + Map flattened = null; + + Iterator> iterator = values.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + BeanProperty beanProperty = desc.getBeanProperty(entry.getKey()); + if (beanProperty instanceof BeanPropertyAssocMany) { + // filter out assoc many bean properties + iterator.remove(); + + } else if (beanProperty instanceof BeanPropertyAssocOne) { + BeanPropertyAssocOne assoc = (BeanPropertyAssocOne)beanProperty; + if (!assoc.isEmbedded()) { + // flatten for assoc one beans + if (flattened == null) { + flattened = new LinkedHashMap(); + } + flattenToId(flattened, entry, beanProperty, assoc); + iterator.remove(); + } + } + } + + if (flattened != null) { + values.putAll(flattened); + } + + return values; + } + + private static void flattenToId(Map flattened, Map.Entry entry, BeanProperty beanProperty, BeanPropertyAssocOne assoc) { + + BeanDescriptor oneDesc = assoc.getTargetDescriptor(); + + ValuePair value = entry.getValue(); + Object newId = value.getNewValue() == null ? null : oneDesc.getId((EntityBean)value.getNewValue()); + Object oldId = value.getOldValue() == null ? null : oneDesc.getId((EntityBean)value.getOldValue()); + + String propName = beanProperty.getName() + "." + oneDesc.getIdProperty().getName(); + flattened.put(propName, new ValuePair(newId, oldId)); + } +} 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 f0b0e0b30..92635429e 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java @@ -9,15 +9,17 @@ 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.event.readaudit.ReadAuditLogger; +import com.avaje.ebean.event.readaudit.ReadAuditPrepare; 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.DefaultChangeLogListener; import com.avaje.ebeaninternal.server.changelog.DefaultChangeLogPrepare; import com.avaje.ebeaninternal.server.changelog.DefaultChangeLogRegister; -import com.avaje.ebeaninternal.server.changelog.DefaultChangeLogListener; import com.avaje.ebeaninternal.server.cluster.ClusterManager; import com.avaje.ebeaninternal.server.deploy.BeanDescriptorManager; import com.avaje.ebeaninternal.server.deploy.DeployOrmXml; @@ -32,6 +34,8 @@ import com.avaje.ebeaninternal.server.persist.DefaultPersister; import com.avaje.ebeaninternal.server.query.CQueryEngine; import com.avaje.ebeaninternal.server.query.DefaultOrmQueryEngine; import com.avaje.ebeaninternal.server.query.DefaultRelationalQueryEngine; +import com.avaje.ebeaninternal.server.readaudit.DefaultReadAuditLogger; +import com.avaje.ebeaninternal.server.readaudit.DefaultReadAuditPrepare; import com.avaje.ebeaninternal.server.resource.ResourceManager; import com.avaje.ebeaninternal.server.resource.ResourceManagerFactory; import com.avaje.ebeaninternal.server.text.json.DJsonContext; @@ -55,8 +59,6 @@ import java.util.Map; /** * Used to extend the ServerConfig with additional objects used to configure and * construct an EbeanServer. - * - * @author rbygrave */ public class InternalConfiguration { @@ -106,8 +108,8 @@ public class InternalConfiguration { private final List plugins = new ArrayList(); public InternalConfiguration(XmlConfig xmlConfig, ClusterManager clusterManager, - ServerCacheManager cacheManager, SpiBackgroundExecutor backgroundExecutor, - ServerConfig serverConfig, BootupClasses bootupClasses) { + ServerCacheManager cacheManager, SpiBackgroundExecutor backgroundExecutor, + ServerConfig serverConfig, BootupClasses bootupClasses) { this.jsonFactory = serverConfig.getJsonFactory(); this.xmlConfig = xmlConfig; @@ -157,7 +159,7 @@ public class InternalConfiguration { */ public T plugin(T maybePlugin) { if (maybePlugin instanceof SpiServerPlugin) { - plugins.add((SpiServerPlugin)maybePlugin); + plugins.add((SpiServerPlugin) maybePlugin); } return maybePlugin; } @@ -191,6 +193,22 @@ public class InternalConfiguration { return plugin((listener != null) ? listener : new DefaultChangeLogListener()); } + /** + * Return the ReadAuditLogger implementation to use. + */ + public ReadAuditLogger getReadAuditLogger() { + ReadAuditLogger found = bootupClasses.getReadAuditLogger(); + return plugin(found != null ? found : new DefaultReadAuditLogger()); + } + + /** + * Return the ReadAuditPrepare implementation to use. + */ + public ReadAuditPrepare getReadAuditPrepare() { + ReadAuditPrepare found = bootupClasses.getReadAuditPrepare(); + return plugin(found != null ? found : new DefaultReadAuditPrepare()); + } + /** * For 'As Of' queries return the number of bind variables per predicate. */ @@ -206,14 +224,14 @@ public class InternalConfiguration { * Create the TransactionManager taking into account autoCommit mode. */ private TransactionManager createTransactionManager() { - + if (isAutoCommitMode()) { return new AutoCommitTransactionManager(clusterManager, backgroundExecutor, serverConfig, beanDescriptorManager, this.getBootupClasses()); } - + return new TransactionManager(clusterManager, backgroundExecutor, serverConfig, beanDescriptorManager, this.getBootupClasses()); } - + /** * Return true if autoCommit mode is on. */ @@ -251,7 +269,6 @@ public class InternalConfiguration { return new DefaultPersister(server, binder, beanDescriptorManager); } - public ServerCacheManager getCacheManager() { return cacheManager; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java b/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java index 00ec3564d..37d6baa58 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java @@ -1,5 +1,7 @@ package com.avaje.ebeaninternal.server.core; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; @@ -374,7 +376,37 @@ public final class OrmQueryRequest extends BeanRequest implements BeanQueryRe cacheKey = query.queryHash(); - return beanDescriptor.queryCacheGet(cacheKey); + BeanCollection cached = beanDescriptor.queryCacheGet(cacheKey); + + if (cached != null && isAuditReads() && readAuditQueryType()) { + // raw sql can't use L2 cache so normal queries only in here + Collection actualDetails = cached.getActualDetails(); + List ids = new ArrayList(actualDetails.size()); + for (T bean : actualDetails) { + ids.add(beanDescriptor.getIdForJson(bean)); + } + beanDescriptor.readAuditMany(queryPlanHash.getPartialKey(), "l2-query-cache", ids); + } + + return cached; + } + + /** + * Return true if the query type contains bean data (not just ids etc) and hence we want to include + * it in read auditing. Return false for row count and find ids queries. + */ + private boolean readAuditQueryType() { + Type type = query.getType(); + switch (type) { + case BEAN: + case ITERATE: + case LIST: + case SET: + case MAP: + return true; + default: + return false; + } } public void putToQueryCache(BeanCollection queryResult) { @@ -410,4 +442,14 @@ public final class OrmQueryRequest extends BeanRequest implements BeanQueryRe int batchSize = query.getLazyLoadBatchSize(); return (batchSize > 0) ? batchSize : ebeanServer.getLazyLoadBatchSize(); } + + /** + * Return true if read auditing is on for this query request. + *

+ * This means that read audit is on for this bean type and that query has not explicitly disabled it. + *

+ */ + public boolean isAuditReads() { + return !query.isDisableReadAudit() && beanDescriptor.isReadAuditing(); + } } 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 e73218217..8455aaf96 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java @@ -19,6 +19,9 @@ 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.event.readaudit.ReadAuditLogger; +import com.avaje.ebean.event.readaudit.ReadAuditPrepare; +import com.avaje.ebean.event.readaudit.ReadEvent; import com.avaje.ebean.meta.MetaBeanInfo; import com.avaje.ebean.meta.MetaQueryPlanStatistic; import com.avaje.ebean.plugin.SpiBeanType; @@ -31,7 +34,7 @@ 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.DiffHelpUpdate; import com.avaje.ebeaninternal.server.core.InternString; import com.avaje.ebeaninternal.server.core.PersistRequest; import com.avaje.ebeaninternal.server.core.PersistRequestBean; @@ -143,6 +146,11 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { private final String baseTableVersionsBetween; private final boolean historySupport; + /** + * Set to true if read auditing is on for this bean type. + */ + private final boolean readAuditing; + /** * Map of BeanProperty Linked so as to preserve order. */ @@ -362,6 +370,7 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { this.updateChangesOnly = deploy.isUpdateChangesOnly(); this.compoundUniqueConstraints = deploy.getCompoundUniqueConstraints(); + this.readAuditing = deploy.isReadAuditing(); this.historySupport = deploy.isHistorySupport(); this.baseTable = InternString.intern(deploy.getBaseTable()); this.baseTableAsOf = deploy.getBaseTableAsOf(); @@ -608,6 +617,20 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { } } + /** + * Return the ReadAuditLogger for logging read audit events. + */ + public ReadAuditLogger getReadAuditLogger() { + return ebeanServer.getReadAuditLogger(); + } + + /** + * Return the ReadAuditPrepare for preparing read audit events prior to logging. + */ + public ReadAuditPrepare getReadAuditPrepare() { + return ebeanServer.getReadAuditPrepare(); + } + /** * Return true if this request should be included in the change log. */ @@ -641,27 +664,20 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { * Return the bean change for an update. */ private BeanChange updateBeanChange(PersistRequestBean request) { - return newBeanChange(request.getBeanId(), ChangeType.UPDATE, request.getEntityBeanIntercept().getDirtyValues()); + return newBeanChange(request.getBeanId(), ChangeType.UPDATE, diffFlatten(request.getEntityBeanIntercept().getDirtyValues())); } /** * Return the bean change for an insert. */ private BeanChange insertBeanChange(PersistRequestBean request) { - return newBeanChange(request.getBeanId(), ChangeType.INSERT, insertDiff(request.getEntityBean())); + return newBeanChange(request.getBeanId(), ChangeType.INSERT, diffForInsert(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. */ @@ -947,10 +963,55 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { public void cacheHandleUpdate(Object id, PersistRequestBean updateRequest) { cacheHelp.handleUpdate(id, updateRequest); } - + /** - * Return the base table alias. This is always the first letter of the bean - * name. + * Prepare the read audit of a findFutureList() query. + */ + public void readAuditFutureList(SpiQuery spiQuery) { + if (isReadAuditing()) { + ReadEvent event = new ReadEvent(fullName); + // prepare in the foreground thread while we have the user context + // information (query is processed/executed later in bg thread) + readAuditPrepare(event); + spiQuery.setFutureFetchAudit(event); + } + } + + /** + * Write a bean read to the read audit log. + */ + public void readAuditBean(String queryKey, String bindLog, Object bean) { + ReadEvent event = new ReadEvent(fullName, queryKey, bindLog, getIdForJson(bean)); + readAuditPrepare(event); + getReadAuditLogger().auditBean(event); + } + + private void readAuditPrepare(ReadEvent event) { + ReadAuditPrepare prepare = getReadAuditPrepare(); + if (prepare != null) { + prepare.prepare(event); + } + } + + /** + * Write a many bean read to the read audit log. + */ + public void readAuditMany(String queryKey, String bindLog, List ids) { + ReadEvent event = new ReadEvent(fullName, queryKey, bindLog, ids); + readAuditPrepare(event); + getReadAuditLogger().auditMany(event); + } + + /** + * Write a futureList many read to the read audit log. + */ + public void readAuditFutureMany(ReadEvent event) { + // this has already been prepared (in foreground thread) + getReadAuditLogger().auditMany(event); + } + + /** + * Return the base table alias. This is always the first letter of the bean name. */ public String getBaseTableAlias() { return baseTableAlias; @@ -1208,6 +1269,9 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { if (d != null) { Object shareableBean = d.getSharableBean(); if (shareableBean != null) { + if (isReadAuditing()) { + readAuditBean("ref", "", shareableBean); + } return (T) shareableBean; } } @@ -1362,6 +1426,26 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { return getId((EntityBean) bean); } + /** + * Return the Id value for the bean with embeddedId beans converted into maps. + *

+ * The usage is to provide simple id types for JSON processing (for embeddedId's). + *

+ */ + public Object getIdForJson(Object bean) { + return idBinder.getIdForJson((EntityBean) bean); + } + + /** + * Convert the idValue assuming embeddedId values are Maps. + *

+ * The usage is to provide simple id types for JSON processing (for embeddedId's). + *

+ */ + public Object convertIdFromJson(Object idValue) { + return idBinder.convertIdFromJson(idValue); + } + /** * Return the default order by that may need to be added if a many property is * included in the query. @@ -1374,7 +1458,7 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { * Convert the type of the idValue if required. */ public Object convertId(Object idValue) { - return idBinder.convertSetId(idValue, null); + return idBinder.convertId(idValue); } /** @@ -1777,6 +1861,13 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { } } + /** + * Return true if read auditing is on this entity bean. + */ + public boolean isReadAuditing() { + return readAuditing; + } + /** * Return true if this entity bean has history support. */ @@ -1943,6 +2034,61 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { } } + /** + * Flatten the diff that comes from the entity bean intercept. + */ + Map diffFlatten(Map diff) { + return DiffHelpUpdate.flatten(diff, this); + } + + /** + * 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 Map diffForInsert(EntityBean newBean) { + + Map map = new LinkedHashMap(); + diffForInsert(null, map, newBean); + return map; + } + + /** + * Populate the diff for inserts with flattened non-null property values. + */ + public void diffForInsert(String prefix, Map map, EntityBean newBean) { + for (int i = 0; i < propertiesBaseScalar.length; i++) { + propertiesBaseScalar[i].diffForInsert(prefix, map, newBean); + } + for (int i = 0; i < propertiesOne.length; i++) { + propertiesOne[i].diffForInsert(prefix, map, newBean); + } + for (int i = 0; i < propertiesEmbedded.length; i++) { + propertiesEmbedded[i].diffForInsert(prefix, map, newBean); + } + } + + /** + * Populate the diff for updates with flattened non-null property values. + */ + public void diff(String prefix, Map map, EntityBean newBean, EntityBean oldBean) { + + for (int i = 0; i < propertiesBaseScalar.length; i++) { + propertiesBaseScalar[i].diff(prefix, map, newBean, oldBean); + } + for (int i = 0; i < propertiesOne.length; i++) { + propertiesOne[i].diff(prefix, map, newBean, oldBean); + } + for (int i = 0; i < propertiesEmbedded.length; i++) { + propertiesEmbedded[i].diff(prefix, map, newBean, oldBean); + } + } + /** * All the BeanPropertyAssocOne that are not embedded. These are effectively * joined beans. For ManyToOne and OneToOne associations. diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java index 7db3505e6..bb2c928a8 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java @@ -432,6 +432,9 @@ public final class BeanDescriptorCacheHelp { if (beanLog.isTraceEnabled()) { beanLog.trace(" GET {}({}) - hit shared bean", cacheName, id); } + if (desc.isReadAuditing()) { + desc.readAuditBean("l2", "", bean); + } return (T) bean; } } @@ -450,6 +453,9 @@ public final class BeanDescriptorCacheHelp { if (beanLog.isTraceEnabled()) { beanLog.trace(" GET {}({}) - hit", cacheName, id); } + if (desc.isReadAuditing()) { + desc.readAuditBean("l2", "", bean); + } return (T) bean; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java index f763f8593..3ae1978d5 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java @@ -1,5 +1,6 @@ package com.avaje.ebeaninternal.server.deploy; +import com.avaje.ebean.ValuePair; import com.avaje.ebean.bean.EntityBean; import com.avaje.ebean.config.EncryptKey; import com.avaje.ebean.config.dbplatform.DbEncryptFunction; @@ -21,6 +22,7 @@ import com.avaje.ebeaninternal.server.text.json.ReadJson; import com.avaje.ebeaninternal.server.text.json.WriteJson; import com.avaje.ebeaninternal.server.type.DataBind; import com.avaje.ebeaninternal.server.type.ScalarType; +import com.avaje.ebeaninternal.util.ValueUtil; import com.fasterxml.jackson.core.JsonToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +35,7 @@ import java.lang.reflect.Field; import java.sql.SQLException; import java.sql.Types; import java.util.List; +import java.util.Map; /** * Description of a property of a bean. Includes its deployment information such @@ -1098,4 +1101,34 @@ public class BeanProperty implements ElPropertyValue { } } } + + /** + * Populate diff map for insert if the property is not null. + */ + public void diffForInsert(String prefix, Map map, EntityBean newBean) { + Object newVal = (newBean == null) ? null : getValue(newBean); + if (newVal != null) { + String propName = (prefix == null) ? name : prefix + "." + name; + map.put(propName, new ValuePair(newVal, null)); + } + } + + /** + * Populate diff map comparing the property values between the beans. + */ + public void diff(String prefix, Map map, EntityBean newBean, EntityBean oldBean) { + Object newVal = (newBean == null) ? null : getValue(newBean); + Object oldVal = (oldBean == null) ? null : getValue(oldBean); + diffVal(prefix, map, newVal, oldVal); + } + + /** + * Populate diff map comparing the property values. + */ + public void diffVal(String prefix, Map map, Object newVal, Object oldVal) { + if (!ValueUtil.areEqual(newVal, oldVal)) { + String propName = (prefix == null) ? name : prefix + "." + name; + map.put(propName, new ValuePair(newVal, oldVal)); + } + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocOne.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocOne.java index 8c8e3a56d..d0de87896 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocOne.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocOne.java @@ -4,6 +4,7 @@ import com.avaje.ebean.EbeanServer; import com.avaje.ebean.Query; import com.avaje.ebean.SqlUpdate; import com.avaje.ebean.Transaction; +import com.avaje.ebean.ValuePair; import com.avaje.ebean.bean.EntityBean; import com.avaje.ebean.bean.EntityBeanIntercept; import com.avaje.ebean.bean.PersistenceContext; @@ -26,6 +27,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Property mapped to a joined bean. @@ -318,6 +320,53 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { return importedPrimaryKey; } + @Override + public void diffForInsert(String prefix, Map map, EntityBean newBean) { + Object newEmb = (newBean == null) ? null : getValue(newBean); + if (newEmb != null) { + prefix = (prefix == null) ? name : prefix + "." + name; + if (embedded) { + getTargetDescriptor().diffForInsert(prefix, map, (EntityBean) newEmb); + } else { + // we are only interested in the Id value + BeanDescriptor targetDescriptor = getTargetDescriptor(); + BeanProperty idProperty = targetDescriptor.getIdProperty(); + idProperty.diffForInsert(prefix, map, (EntityBean) newEmb); + } + } + } + + @Override + public void diff(String prefix, Map map, EntityBean newBean, EntityBean oldBean) { + + Object newEmb = (newBean == null) ? null : getValue(newBean); + Object oldEmb = (oldBean == null) ? null : getValue(oldBean); + if (newEmb == null && oldEmb == null) { + return; + } + + if (embedded) { + prefix = (prefix == null) ? name : prefix + "." + name; + BeanDescriptor targetDescriptor = getTargetDescriptor(); + targetDescriptor.diff(prefix, map, (EntityBean) newEmb, (EntityBean) oldEmb); + + } else { + // we are only interested in the Id value + newBean = (EntityBean)newEmb; + oldBean = (EntityBean)oldEmb; + + BeanDescriptor targetDescriptor = getTargetDescriptor(); + BeanProperty idProperty = targetDescriptor.getIdProperty(); + + Object newId = (newBean == null) ? null : idProperty.getValue(newBean); + Object oldId = (oldBean == null) ? null : idProperty.getValue(oldBean); + if (newId != null || oldId != null) { + prefix = (prefix == null) ? name : prefix + "." + name; + idProperty.diffVal(prefix, map, newId, oldId); + } + } + } + /** * Same as getPropertyType(). Return the type of the bean this property * represents. diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/id/IdBinder.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/id/IdBinder.java index 3d3839367..707dfca5e 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/id/IdBinder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/id/IdBinder.java @@ -84,6 +84,28 @@ public interface IdBinder { */ Object[] getBindValues(Object idValue); + + /** + * For EmbeddedId convert the idValue into a simple map. + * Otherwise the idValue is just returned as is. + *

+ * This is used to provide a simple JSON serializable version of the id value. + *

+ */ + Object getIdForJson(EntityBean idValue); + + /** + * For EmbeddedId the value is assumed to be a Map and this is + * takes the values from the map and builds an embedded id bean. + *

+ * For other simple id's this just returns the value (no conversion required). + *

+ *

+ * This is used to provide a simple JSON serializable version of the id value. + *

+ */ + Object convertIdFromJson(Object value); + /** * Return the id values for a given bean. */ @@ -171,4 +193,8 @@ public interface IdBinder { */ Object convertSetId(Object idValue, EntityBean bean); + /** + * Cast or convert the Id value if necessary. + */ + Object convertId(Object idValue); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/id/IdBinderEmbedded.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/id/IdBinderEmbedded.java index 990720623..1dc7f4a88 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/id/IdBinderEmbedded.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/id/IdBinderEmbedded.java @@ -15,7 +15,9 @@ import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.sql.SQLException; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** * Bind an Id that is an Embedded bean. @@ -225,6 +227,36 @@ public final class IdBinderEmbedded implements IdBinder { return bindvalues; } + /** + * Convert from embedded bean to Map. + */ + @Override + public Object getIdForJson(EntityBean bean) { + + EntityBean ebValue = (EntityBean)embIdProperty.getValue(bean); + Map map = new LinkedHashMap(); + for (int i = 0; i < props.length; i++) { + map.put(props[i].getName(), props[i].getValue(ebValue)); + } + return map; + } + + /** + * Convert back from a Map to embedded bean. + */ + public Object convertIdFromJson(Object value) { + + Map map = (Map)value; + + EntityBean idValue = idDesc.createEntityBean(); + for (int i = 0; i < props.length; i++) { + Object val = map.get(props[i].getName()); + props[i].setValue(idValue, val); + } + return idValue; + } + + public void bindId(DefaultSqlUpdate sqlUpdate, Object value) { for (int i = 0; i < props.length; i++) { Object embFieldValue = props[i].getValue((EntityBean) value); @@ -386,6 +418,12 @@ public final class IdBinderEmbedded implements IdBinder { return sb.toString(); } + @Override + public Object convertId(Object idValue) { + // can not cast/convert if it is embedded + return idValue; + } + public Object convertSetId(Object idValue, EntityBean bean) { // can not cast/convert if it is embedded diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/id/IdBinderEmpty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/id/IdBinderEmpty.java index 268f7906b..0d60ac68d 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/id/IdBinderEmpty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/id/IdBinderEmpty.java @@ -98,6 +98,16 @@ public final class IdBinderEmpty implements IdBinder { return new Object[]{idValue}; } + @Override + public Object getIdForJson(EntityBean bean) { + return null; + } + + @Override + public Object convertIdFromJson(Object value) { + return value; + } + public void bindId(DefaultSqlUpdate sqlUpdate, Object value) { } @@ -124,6 +134,11 @@ public final class IdBinderEmpty implements IdBinder { return idValue; } + @Override + public Object convertId(Object idValue) { + return idValue; + } + public Object readData(DataInput dataOutput) throws IOException { return null; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/id/IdBinderSimple.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/id/IdBinderSimple.java index 46e4b0f03..0a3f05474 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/id/IdBinderSimple.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/id/IdBinderSimple.java @@ -131,6 +131,17 @@ public final class IdBinderSimple implements IdBinder { request.addBindValue(value); } + @Override + public Object getIdForJson(EntityBean bean) { + return idProperty.getValue(bean); + } + + @Override + public Object convertIdFromJson(Object value) { + // handle simple type conversion if required + return convertId(value); + } + public void bindId(DefaultSqlUpdate sqlUpdate, Object value) { sqlUpdate.addParameter(value); } @@ -193,6 +204,14 @@ public final class IdBinderSimple implements IdBinder { return sb.toString(); } + public Object convertId(Object idValue) { + + if (!idValue.getClass().equals(expectedType)) { + return scalarType.toBeanType(idValue); + } + return idValue; + } + public Object convertSetId(Object idValue, EntityBean bean) { if (!idValue.getClass().equals(expectedType)) { 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 e478f7644..2489e75ab 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 @@ -120,6 +120,8 @@ public class DeployBeanDescriptor { private boolean historySupport; + private boolean readAuditing; + private TableName baseTableFull; private String[] properties; @@ -185,6 +187,20 @@ public class DeployBeanDescriptor { return historySupport; } + /** + * Set read auditing on for this entity bean. + */ + public void setReadAuditing() { + readAuditing = true; + } + + /** + * Return true if read auditing is on for this entity bean. + */ + public boolean isReadAuditing() { + return readAuditing; + } + public boolean isScalaObject() { Class[] interfaces = beanType.getInterfaces(); for (int i = 0; i < interfaces.length; i++) { @@ -741,7 +757,7 @@ public class DeployBeanDescriptor { } DeployBeanProperty p = ids.get(0); if (p instanceof DeployBeanPropertyAssocOne) { - return ((DeployBeanPropertyAssocOne)p).isCompound(); + return ((DeployBeanPropertyAssocOne) p).isCompound(); } else { return !p.isDbNumberType(); } @@ -859,17 +875,17 @@ public class DeployBeanDescriptor { } /** - * Check valid mapping annotations on the class hierarchy. + * Check valid mapping annotations on the class hierarchy. */ private void checkInheritance(Class beanType) { - + Class parent = beanType.getSuperclass(); if (parent == null || Object.class.equals(parent)) { // all good return; } if (parent.isAnnotationPresent(Entity.class)) { - String msg = "Checking "+getBeanType()+" and found "+parent+" that has @Entity annotation rather than MappedSuperclass?"; + String msg = "Checking " + getBeanType() + " and found " + parent + " that has @Entity annotation rather than MappedSuperclass?"; throw new IllegalStateException(msg); } if (parent.isAnnotationPresent(MappedSuperclass.class)) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationClass.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationClass.java index 901e2b85b..c869a4339 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationClass.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationClass.java @@ -14,6 +14,7 @@ import com.avaje.ebean.annotation.History; import com.avaje.ebean.annotation.Index; import com.avaje.ebean.annotation.NamedUpdate; import com.avaje.ebean.annotation.NamedUpdates; +import com.avaje.ebean.annotation.ReadAudit; import com.avaje.ebean.annotation.UpdateMode; import com.avaje.ebean.config.TableName; import com.avaje.ebeaninternal.server.core.CacheOptions; @@ -97,6 +98,11 @@ public class AnnotationClass extends AnnotationParser { } } + ReadAudit readAudit = cls.getAnnotation(ReadAudit.class); + if (readAudit != null) { + descriptor.setReadAuditing(); + } + History history = cls.getAnnotation(History.class); if (history != null) { descriptor.setHistorySupport(); diff --git a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java index b1f7adada..175d27ac6 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java @@ -54,6 +54,9 @@ public class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContex query.asOf(parent.getAsOf()); query.setParentNode(objectGraphNode); query.setLazyLoadProperty(lazyLoadProperty); + if (parent.isDisableReadAudit()) { + query.setDisableReadAuditing(); + } if (queryProps != null) { queryProps.configureBeanQuery(query); diff --git a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadContext.java b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadContext.java index f9f1c1870..ae7924217 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadContext.java @@ -41,6 +41,7 @@ public class DLoadContext implements LoadContext { private final boolean excludeBeanCache; private final int defaultBatchSize; private final boolean disableLazyLoading; + private final boolean disableReadAudit; /** * The path relative to the root of the object graph. @@ -65,6 +66,7 @@ public class DLoadContext implements LoadContext { SpiQuery query = request.getQuery(); this.asOf = query.getAsOf(); this.readOnly = query.isReadOnly(); + this.disableReadAudit = query.isDisableReadAudit(); this.disableLazyLoading = query.isDisableLazyLoading(); this.excludeBeanCache = Boolean.FALSE.equals(query.isUseBeanCache()); this.useAutofetchManager = query.getAutoFetchManager() != null; @@ -221,10 +223,23 @@ public class DLoadContext implements LoadContext { return readOnly; } + /** + * Return the 'as of' timestamp that should propagate to secondary queries. + */ protected Timestamp getAsOf() { return asOf; } + /** + * Return true if disable read auditing should propagate to secondary queries. + */ + protected boolean isDisableReadAudit() { + return disableReadAudit; + } + + /** + * Return true if disable lazy loading should propagate to secondary queries. + */ protected boolean isDisableLazyLoading() { return disableLazyLoading; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java index 2e726ee94..1100e7f1b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java @@ -65,6 +65,9 @@ public class DLoadManyContext extends DLoadBaseContext implements LoadManyContex query.setDisableLazyLoading(parent.isDisableLazyLoading()); query.asOf(parent.getAsOf()); query.setParentNode(objectGraphNode); + if (parent.isDisableReadAudit()) { + query.setDisableReadAuditing(); + } if (queryProps != null) { queryProps.configureBeanQuery(query); diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java index 1f0518c1c..cc52ef6bb 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java @@ -2,7 +2,14 @@ package com.avaje.ebeaninternal.server.query; import com.avaje.ebean.QueryIterator; import com.avaje.ebean.Version; -import com.avaje.ebean.bean.*; +import com.avaje.ebean.bean.BeanCollection; +import com.avaje.ebean.bean.EntityBean; +import com.avaje.ebean.bean.EntityBeanIntercept; +import com.avaje.ebean.bean.NodeUsageCollector; +import com.avaje.ebean.bean.NodeUsageListener; +import com.avaje.ebean.bean.ObjectGraphNode; +import com.avaje.ebean.bean.PersistenceContext; +import com.avaje.ebean.event.readaudit.ReadEvent; import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.api.SpiQuery.Mode; import com.avaje.ebeaninternal.api.SpiTransaction; @@ -10,7 +17,11 @@ import com.avaje.ebeaninternal.server.autofetch.AutoFetchManager; import com.avaje.ebeaninternal.server.core.Message; import com.avaje.ebeaninternal.server.core.OrmQueryRequest; import com.avaje.ebeaninternal.server.core.SpiOrmQueryRequest; -import com.avaje.ebeaninternal.server.deploy.*; +import com.avaje.ebeaninternal.server.deploy.BeanCollectionHelp; +import com.avaje.ebeaninternal.server.deploy.BeanCollectionHelpFactory; +import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; +import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocMany; +import com.avaje.ebeaninternal.server.deploy.DbReadContext; import com.avaje.ebeaninternal.server.lib.util.StringHelper; import com.avaje.ebeaninternal.server.type.DataBind; import com.avaje.ebeaninternal.server.type.DataReader; @@ -24,6 +35,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -163,6 +175,16 @@ public class CQuery implements DbReadContext, CancelableQuery { private long executionTimeMicros; + /** + * Flag set when findIterate is being read audited. + */ + private boolean auditFindIterate; + + /** + * A buffer of Ids collected for findIterate auditing. + */ + private List auditFindIterateIds; + /** * Create the Sql select based on the request. */ @@ -295,7 +317,7 @@ public class CQuery implements DbReadContext, CancelableQuery { } if (forwardOnlyHint) { - // Use forward only hints for large resultset processing (Issue 56, MySql specific) + // Use forward only hints for large resultSet processing (Issue 56, MySql specific) pstmt = conn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); pstmt.setFetchSize(Integer.MIN_VALUE); } else { @@ -327,18 +349,24 @@ public class CQuery implements DbReadContext, CancelableQuery { /** * Close the resources. *

- * The jdbc resultSet and statement need to be closed. Its important that this - * method is called. + * The JDBC resultSet and statement need to be closed. Its important that this method is called. *

*/ public void close() { + try { + if (auditFindIterateIds != null && !auditFindIterateIds.isEmpty()) { + auditIterateLogMessage(); + } + } catch (Throwable e) { + logger.error("Error logging read audit logs", e); + } try { if (dataReader != null) { dataReader.close(); dataReader = null; } } catch (SQLException e) { - logger.error(null, e); + logger.error("Error closing dataReader", e); } try { if (pstmt != null) { @@ -346,7 +374,7 @@ public class CQuery implements DbReadContext, CancelableQuery { pstmt = null; } } catch (SQLException e) { - logger.error(null, e); + logger.error("Error closing preparedStatement", e); } } @@ -464,6 +492,9 @@ public class CQuery implements DbReadContext, CancelableQuery { } protected EntityBean next() { + if (auditFindIterate) { + auditIterateNextBean(); + } return nextBean; } @@ -610,8 +641,7 @@ public class CQuery implements DbReadContext, CancelableQuery { } /** - * Create a PersistenceException including interesting information like the - * bindLog and sql used. + * Create a PersistenceException including interesting information like the bindLog and sql used. */ public PersistenceException createPersistenceException(SQLException e) { @@ -619,8 +649,7 @@ public class CQuery implements DbReadContext, CancelableQuery { } /** - * Create a PersistenceException including interesting information like the - * bindLog and sql used. + * Create a PersistenceException including interesting information like the bindLog and sql used. */ public static PersistenceException createPersistenceException(SQLException e, SpiTransaction t, String bindLog, String sql) { @@ -680,4 +709,71 @@ public class CQuery implements DbReadContext, CancelableQuery { this.currentPathMap = currentPathMap; } + /** + * A find bean query with read auditing so build and log the ReadEvent. + */ + public void auditFind(EntityBean bean) { + if (bean != null) { + // only audit when a bean was actually found + desc.readAuditBean(queryPlan.getAuditQueryKey(), bindLog, bean); + } + } + + /** + * a find many query with read auditing so build the ReadEvent and log it. + */ + public void auditFindMany() { + + if (!collection.isEmpty()) { + // get the id values of the underlying collection + List ids = new ArrayList(collection.size()); + Collection underlyingBeans = collection.getActualDetails(); + for (T underlyingBean : underlyingBeans) { + ids.add(desc.getIdForJson(underlyingBean)); + } + ReadEvent futureReadEvent = query.getFutureFetchAudit(); + if (futureReadEvent == null) { + // normal query execution + desc.readAuditMany(queryPlan.getAuditQueryKey(), bindLog, ids); + } else { + // this query was executed via findFutureList() and the prepare() + // has already been called so set the details and log + futureReadEvent.setQueryKey(queryPlan.getAuditQueryKey()); + futureReadEvent.setBindLog(bindLog); + futureReadEvent.setIds(ids); + desc.readAuditFutureMany(futureReadEvent); + } + } + } + + /** + * Indicate that read auditing is occurring on a this findIterate query. + */ + public void auditFindIterate() { + auditFindIterate = true; + } + + /** + * Send the current buffer of findIterate collected ids to the audit log. + */ + private void auditIterateLogMessage() { + desc.readAuditMany(queryPlan.getAuditQueryKey(), bindLog, auditFindIterateIds); + // create a new list on demand with the next bean/id + auditFindIterateIds = null; + } + + /** + * Add the id to the audit id buffer and flush if needed in batches of 100. + */ + private void auditIterateNextBean() { + + if (auditFindIterateIds == null) { + auditFindIterateIds = new ArrayList(100); + } + auditFindIterateIds.add(desc.getIdForJson(nextBean)); + if (auditFindIterateIds.size() >= 100) { + auditIterateLogMessage(); + } + } + } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java index 40350fa77..56ac874bf 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java @@ -8,6 +8,7 @@ import com.avaje.ebean.config.dbplatform.DatabasePlatform; import com.avaje.ebean.config.dbplatform.SqlLimitRequest; import com.avaje.ebean.config.dbplatform.SqlLimitResponse; import com.avaje.ebean.config.dbplatform.SqlLimiter; +import com.avaje.ebean.event.readaudit.ReadAuditQueryPlan; import com.avaje.ebean.text.PathProperties; import com.avaje.ebeaninternal.api.ManyWhereJoins; import com.avaje.ebeaninternal.api.SpiQuery; @@ -269,6 +270,12 @@ public class CQueryBuilder { queryPlan = new CQueryPlan(request, res, sqlTree, false, predicates.getLogWhereSql()); } + BeanDescriptor desc = request.getBeanDescriptor(); + if (desc.isReadAuditing()) { + // log the query plan based bean type (i.e. ignoring query disabling for logging the sql/plan) + desc.getReadAuditLogger().queryPlan(new ReadAuditQueryPlan(desc.getFullName(), queryPlan.getAuditQueryKey(), queryPlan.getSql())); + } + // cache the query plan because we can reuse it and also // gather query performance statistics based on it. request.putQueryPlan(queryPlan); diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java index 49fa7e74a..8288ef9f4 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryEngine.java @@ -180,6 +180,11 @@ public class CQueryEngine { logFindManySummary(cquery); } + if (request.isAuditReads()) { + // indicates we need to audit as the iterator progresses + cquery.auditFindIterate(); + } + return readIterate; } catch (SQLException e) { @@ -218,6 +223,10 @@ public class CQueryEngine { logFindManySummary(cquery); } + if (request.isAuditReads()) { + cquery.auditFindMany(); + } + return versions; } catch (SQLException e) { @@ -297,6 +306,10 @@ public class CQueryEngine { logFindManySummary(cquery); } + if (request.isAuditReads()) { + cquery.auditFindMany(); + } + request.executeSecondaryQueries(); return beanCollection; @@ -342,6 +355,10 @@ public class CQueryEngine { logFindBeanSummary(cquery); } + if (request.isAuditReads()) { + cquery.auditFind(bean); + } + request.executeSecondaryQueries(); return (T) bean; diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPlan.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPlan.java index bac244856..f1e3c8d45 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPlan.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPlan.java @@ -1,5 +1,6 @@ package com.avaje.ebeaninternal.server.query; +import java.security.MessageDigest; import java.sql.ResultSet; import java.sql.SQLException; @@ -14,6 +15,8 @@ import com.avaje.ebeaninternal.server.query.CQueryPlanStats.Snapshot; import com.avaje.ebeaninternal.server.type.DataBind; import com.avaje.ebeaninternal.server.type.DataReader; import com.avaje.ebeaninternal.server.type.RsetDataReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Represents a query for a given SQL statement. @@ -36,6 +39,8 @@ import com.avaje.ebeaninternal.server.type.RsetDataReader; */ public class CQueryPlan { + private static final Logger logger = LoggerFactory.getLogger(CQueryPlan.class); + private final SpiEbeanServer server; private final boolean autofetchTuned; @@ -61,6 +66,11 @@ public class CQueryPlan { private final Class beanType; + /** + * Key used to identify the query plan in audit logging. + */ + private volatile String auditQueryHash; + /** * Create a query plan based on a OrmQueryRequest. */ @@ -141,6 +151,48 @@ public class CQueryPlan { return hash; } + /** + * Return a key used in audit logging to identify the query. + */ + public String getAuditQueryKey() { + if (auditQueryHash == null) { + // volatile object assignment (so happy for multithreaded access) + auditQueryHash = calcAuditQueryKey(); + } + return auditQueryHash; + } + + private String calcAuditQueryKey() { + // rawSql needs to include the MD5 hash of the sql + return rawSql ? hash.getPartialKey() + "_" + getSqlMd5Hash() : hash.getPartialKey(); + } + + /** + * Return the MD5 hash of the underlying sql. + */ + private String getSqlMd5Hash() { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(sql.getBytes("UTF-8")); + return digestToHex(digest); + } catch (Exception e) { + logger.error("Failed to MD5 hash the rawSql query", e); + return "error"; + } + } + + /** + * Convert the digest into a hex value. + */ + private String digestToHex(byte[] digest) { + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < digest.length; i++) { + sb.append(Integer.toString((digest[i] & 0xff) + 0x100, 16).substring(1)); + } + return sb.toString(); + } + public String getSql() { return sql; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java index db2dfb342..45fc3cfc6 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -17,6 +17,7 @@ import com.avaje.ebean.bean.ObjectGraphNode; import com.avaje.ebean.bean.ObjectGraphOrigin; import com.avaje.ebean.bean.PersistenceContext; import com.avaje.ebean.event.BeanQueryRequest; +import com.avaje.ebean.event.readaudit.ReadEvent; import com.avaje.ebean.text.PathProperties; import com.avaje.ebeaninternal.api.BindParams; import com.avaje.ebeaninternal.api.HashQuery; @@ -135,6 +136,11 @@ public class DefaultOrmQuery implements SpiQuery { */ private boolean futureFetch; + /** + * Only used for read auditing with findFutureList() query. + */ + private ReadEvent futureFetchAudit; + private List partialIds; private int timeout; @@ -173,6 +179,8 @@ public class DefaultOrmQuery implements SpiQuery { private Timestamp versionsStart; private Timestamp versionsEnd; + private boolean disableReadAudit; + private int bufferFetchSizeHint; private boolean usageProfiling = true; @@ -1320,6 +1328,16 @@ public class DefaultOrmQuery implements SpiQuery { return bufferFetchSizeHint; } + @Override + public Query setDisableReadAuditing() { + this.disableReadAudit = true; + return this; + } + + public boolean isDisableReadAudit() { + return disableReadAudit; + } + public void setBeanCollectionTouched(BeanCollectionTouched notify) { this.beanCollectionTouched = notify; } @@ -1344,6 +1362,16 @@ public class DefaultOrmQuery implements SpiQuery { this.futureFetch = backgroundFetch; } + @Override + public void setFutureFetchAudit(ReadEvent event) { + this.futureFetchAudit = event; + } + + @Override + public ReadEvent getFutureFetchAudit() { + return futureFetchAudit; + } + public void setCancelableQuery(CancelableQuery cancelableQuery) { synchronized (this) { this.cancelableQuery = cancelableQuery; diff --git a/src/main/java/com/avaje/ebeaninternal/server/readaudit/DefaultReadAuditLogger.java b/src/main/java/com/avaje/ebeaninternal/server/readaudit/DefaultReadAuditLogger.java new file mode 100644 index 000000000..6f04e2cb6 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/readaudit/DefaultReadAuditLogger.java @@ -0,0 +1,141 @@ +package com.avaje.ebeaninternal.server.readaudit; + +import com.avaje.ebean.event.readaudit.ReadAuditLogger; +import com.avaje.ebean.event.readaudit.ReadAuditQueryPlan; +import com.avaje.ebean.event.readaudit.ReadEvent; +import com.avaje.ebean.text.json.EJson; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Map; + +/** + * Default implementation of ReadAuditLogger that writes the event in JSON format to standard loggers. + */ +public class DefaultReadAuditLogger implements ReadAuditLogger { + + private static final Logger appLogger = LoggerFactory.getLogger(DefaultReadAuditLogger.class); + + private static final Logger queryLogger = LoggerFactory.getLogger("org.avaje.ebean.ReadAuditQuery"); + + private static final Logger auditLogger = LoggerFactory.getLogger("org.avaje.ebean.ReadAudit"); + + protected final JsonFactory jsonFactory = new JsonFactory(); + + protected int defaultQueryBuffer = 500; + + protected int defaultReadBuffer = 150; + + /** + * Write the query plan details in JSON format to the logger. + */ + @Override + public void queryPlan(ReadAuditQueryPlan queryPlan) { + try { + StringWriter writer = new StringWriter(defaultQueryBuffer); + JsonGenerator gen = jsonFactory.createGenerator(writer); + + gen.writeStartObject(); + String beanType = queryPlan.getBeanType(); + if (beanType != null) { + gen.writeStringField("beanType", beanType); + } + String queryKey = queryPlan.getQueryKey(); + if (queryKey != null) { + gen.writeStringField("queryKey", queryKey); + } + String sql = queryPlan.getSql(); + if (sql != null) { + gen.writeStringField("sql", sql); + } + gen.writeEndObject(); + gen.flush(); + gen.close(); + + queryLogger.info(writer.toString()); + + } catch (IOException e) { + appLogger.error("Error writing Read audit event", e); + } + } + + /** + * Write the bean read event details in JSON format to the logger. + */ + @Override + public void auditBean(ReadEvent beanEvent) { + + writeEvent(beanEvent); + } + + /** + * Write the many beans read event details in JSON format to the logger. + */ + @Override + public void auditMany(ReadEvent readMany) { + writeEvent(readMany); + } + + protected void writeEvent(ReadEvent event) { + + try { + StringWriter writer = new StringWriter(defaultReadBuffer); + JsonGenerator gen = jsonFactory.createGenerator(writer); + writeDetails(gen, event); + + auditLogger.info(writer.toString()); + + } catch (IOException e) { + appLogger.error("Error writing Read audit event", e); + } + } + + /** + * Write the details for the read bean or read many beans event. + */ + protected void writeDetails(JsonGenerator gen, ReadEvent event) throws IOException { + + gen.writeStartObject(); + String source = event.getSource(); + if (source != null) { + gen.writeStringField("source", source); + } + String userId = event.getUserId(); + if (userId != null) { + gen.writeStringField("userId", userId); + } + String userIpAddress = event.getUserIpAddress(); + if (userIpAddress != null) { + gen.writeStringField("userIpAddress", userIpAddress); + } + Map userContext = event.getUserContext(); + if (userContext != null && !userContext.isEmpty()) { + gen.writeObjectFieldStart("userContext"); + for (Map.Entry entry : userContext.entrySet()) { + gen.writeStringField(entry.getKey(), entry.getValue()); + } + gen.writeEndObject(); + } + gen.writeNumberField("eventTime", event.getEventTime()); + gen.writeStringField("beanType", event.getBeanType()); + gen.writeStringField("queryKey", event.getQueryKey()); + gen.writeStringField("bindLog", event.getBindLog()); + Object id = event.getId(); + if (id != null) { + gen.writeFieldName("id"); + EJson.write(id, gen); + } else { + gen.writeFieldName("ids"); + EJson.writeCollection(event.getIds(), gen); + } + + gen.writeEndObject(); + gen.flush(); + gen.close(); + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/readaudit/DefaultReadAuditPrepare.java b/src/main/java/com/avaje/ebeaninternal/server/readaudit/DefaultReadAuditPrepare.java new file mode 100644 index 000000000..6a5e881ca --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/readaudit/DefaultReadAuditPrepare.java @@ -0,0 +1,19 @@ +package com.avaje.ebeaninternal.server.readaudit; + +import com.avaje.ebean.event.readaudit.ReadAuditPrepare; +import com.avaje.ebean.event.readaudit.ReadEvent; + +/** + * A placeholder implementation for ReadAuditPrepare. + *

+ * A real application specific implementation is required to obtain and set + * the user context information on the readEvent bean (like user id). + *

+ */ +public class DefaultReadAuditPrepare implements ReadAuditPrepare { + + @Override + public void prepare(ReadEvent readEvent) { + // do nothing by default. + } +} diff --git a/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java b/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java index 9304cf8ec..6140611ee 100644 --- a/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java +++ b/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java @@ -9,6 +9,8 @@ import com.avaje.ebean.cache.ServerCacheManager; import com.avaje.ebean.config.ServerConfig; import com.avaje.ebean.config.dbplatform.DatabasePlatform; import com.avaje.ebean.dbmigration.DdlGenerator; +import com.avaje.ebean.event.readaudit.ReadAuditLogger; +import com.avaje.ebean.event.readaudit.ReadAuditPrepare; import com.avaje.ebean.meta.MetaInfoManager; import com.avaje.ebean.plugin.SpiServer; import com.avaje.ebean.text.csv.CsvReader; @@ -91,6 +93,16 @@ public class TDSpiEbeanServer implements SpiEbeanServer { return null; } + @Override + public ReadAuditLogger getReadAuditLogger() { + return null; + } + + @Override + public ReadAuditPrepare getReadAuditPrepare() { + return null; + } + @Override public void clearQueryStatistics() { diff --git a/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpInsertSimple.java b/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpInsertSimple.java index 728468799..2c5bc082a 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpInsertSimple.java +++ b/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpInsertSimple.java @@ -51,7 +51,7 @@ public class TestDiffHelpInsertSimple extends BaseTestCase { Order order1 = createBaseOrder(server); - Map diff = DiffHelpInsert.diff((EntityBean) order1, orderDesc); + Map diff = orderDesc.diffForInsert((EntityBean) order1); assertEquals(4, diff.size()); diff --git a/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpInsertWithEmbedded.java b/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpInsertWithEmbedded.java index 6d220f730..3de19e019 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpInsertWithEmbedded.java +++ b/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpInsertWithEmbedded.java @@ -34,7 +34,7 @@ public class TestDiffHelpInsertWithEmbedded extends BaseTestCase { EMain emain1 = createEMain(); - Map diff = DiffHelpInsert.diff((EntityBean) emain1, emainDesc); + Map diff = emainDesc.diffForInsert((EntityBean) emain1); assertEquals(3, diff.size()); assertEquals("foo", diff.get("name").getNewValue()); assertEquals(13L, diff.get("version").getNewValue()); @@ -51,7 +51,7 @@ public class TestDiffHelpInsertWithEmbedded extends BaseTestCase { EMain emain1 = createEMain(); emain1.setName(null); - Map diff = DiffHelpInsert.diff((EntityBean) emain1, emainDesc); + Map diff = emainDesc.diffForInsert((EntityBean) emain1); assertEquals(2, diff.size()); assertNull(diff.get("name")); assertEquals(13L, diff.get("version").getNewValue()); @@ -68,7 +68,7 @@ public class TestDiffHelpInsertWithEmbedded extends BaseTestCase { EMain emain1 = createEMain(); emain1.setEmbeddable(null); - Map diff = DiffHelpInsert.diff((EntityBean) emain1, emainDesc); + Map diff = emainDesc.diffForInsert((EntityBean) emain1); assertEquals(2, diff.size()); assertEquals("foo", diff.get("name").getNewValue()); assertEquals(13L, diff.get("version").getNewValue()); @@ -83,7 +83,7 @@ public class TestDiffHelpInsertWithEmbedded extends BaseTestCase { EMain emain1 = createEMain(); emain1.getEmbeddable().setDescription(null); - Map diff = DiffHelpInsert.diff((EntityBean) emain1, emainDesc); + Map diff = emainDesc.diffForInsert((EntityBean) emain1); assertEquals(2, diff.size()); assertEquals("foo", diff.get("name").getNewValue()); assertEquals(13L, diff.get("version").getNewValue()); diff --git a/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpWithEmbedded.java b/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpWithEmbedded.java index f15686b47..366ed9977 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpWithEmbedded.java +++ b/src/test/java/com/avaje/ebeaninternal/server/core/TestDiffHelpWithEmbedded.java @@ -66,7 +66,25 @@ public class TestDiffHelpWithEmbedded extends BaseTestCase { Assert.assertEquals("bar",valuePair.getNewValue()); Assert.assertEquals("baz",valuePair.getOldValue()); } - + + @Test + public void testDiffEmbedded_flatMode() { + + EMain emain1 = createEMain(); + EMain emain2 = createEMain(); + emain2.getEmbeddable().setDescription("baz"); + + DiffHelp diffHelp = new DiffHelp(true); + + Map diff = diffHelp.diff(emain1, emain2, emainDesc); + Assert.assertEquals(1, diff.size()); + ValuePair valuePair = diff.get("embeddable.description"); + + Assert.assertNotNull(valuePair); + Assert.assertEquals("bar",valuePair.getNewValue()); + Assert.assertEquals("baz",valuePair.getOldValue()); + } + @Test public void testFirstEmbeddedIsNull() { diff --git a/src/test/java/com/avaje/ebeaninternal/server/deploy/TestBeanDescriptorHasIdProperty.java b/src/test/java/com/avaje/ebeaninternal/server/deploy/TestBeanDescriptorHasIdProperty.java index 19dbe0f4a..0d6b90405 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/deploy/TestBeanDescriptorHasIdProperty.java +++ b/src/test/java/com/avaje/ebeaninternal/server/deploy/TestBeanDescriptorHasIdProperty.java @@ -1,10 +1,5 @@ package com.avaje.ebeaninternal.server.deploy; -import java.sql.Timestamp; - -import org.junit.Assert; -import org.junit.Test; - import com.avaje.ebean.BaseTestCase; import com.avaje.ebean.Ebean; import com.avaje.ebean.EbeanServer; @@ -13,6 +8,17 @@ import com.avaje.ebean.bean.EntityBeanIntercept; import com.avaje.ebeaninternal.api.SpiEbeanServer; import com.avaje.tests.model.basic.Customer; import com.avaje.tests.model.basic.Order; +import com.avaje.tests.model.composite.RCustomer; +import com.avaje.tests.model.composite.RCustomerKey; +import org.junit.Test; + +import java.sql.Timestamp; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; public class TestBeanDescriptorHasIdProperty extends BaseTestCase { @@ -27,22 +33,22 @@ public class TestBeanDescriptorHasIdProperty extends BaseTestCase { public void testHasId() { BeanDescriptor beanDescriptor = spiServer.getBeanDescriptor(Order.class); - Assert.assertNotNull(beanDescriptor.getIdProperty()); - Assert.assertEquals("id", beanDescriptor.getIdProperty().getName()); + assertNotNull(beanDescriptor.getIdProperty()); + assertEquals("id", beanDescriptor.getIdProperty().getName()); - Assert.assertNotNull(beanDescriptor.getVersionProperty()); - Assert.assertEquals("updtime", beanDescriptor.getVersionProperty().getName()); + assertNotNull(beanDescriptor.getVersionProperty()); + assertEquals("updtime", beanDescriptor.getVersionProperty().getName()); Order order = new Order(); - Assert.assertFalse(beanDescriptor.hasIdValue(entityBean(order))); - Assert.assertFalse(beanDescriptor.hasVersionProperty(getIntercept(order))); + assertFalse(beanDescriptor.hasIdValue(entityBean(order))); + assertFalse(beanDescriptor.hasVersionProperty(getIntercept(order))); order.setId(23); order.setUpdtime(new Timestamp(System.currentTimeMillis())); - Assert.assertTrue(beanDescriptor.hasIdValue(entityBean(order))); - Assert.assertTrue(beanDescriptor.hasVersionProperty(getIntercept(order))); + assertTrue(beanDescriptor.hasIdValue(entityBean(order))); + assertTrue(beanDescriptor.hasVersionProperty(getIntercept(order))); } @@ -53,13 +59,47 @@ public class TestBeanDescriptorHasIdProperty extends BaseTestCase { Customer order = new Customer(); EntityBeanIntercept ebi = getIntercept(order); - Assert.assertFalse(beanDescriptor.hasIdPropertyOnly(ebi)); + assertFalse(beanDescriptor.hasIdPropertyOnly(ebi)); order.setId(23); - Assert.assertTrue(beanDescriptor.hasIdPropertyOnly(ebi)); + assertTrue(beanDescriptor.hasIdPropertyOnly(ebi)); order.setName("custName"); - Assert.assertFalse(beanDescriptor.hasIdPropertyOnly(ebi)); + assertFalse(beanDescriptor.hasIdPropertyOnly(ebi)); + } + + @Test + public void test_getIdForJson() { + + BeanDescriptor orderDesc = spiServer.getBeanDescriptor(Order.class); + + Order order = new Order(); + order.setId(42); + assertEquals(42, orderDesc.getIdForJson(order)); + + assertEquals(42, orderDesc.convertIdFromJson(42)); + assertEquals(42, orderDesc.convertIdFromJson("42")); + assertEquals(42, orderDesc.convertIdFromJson(42L)); + + + RCustomerKey key = new RCustomerKey(); + key.setCompany("comp"); + key.setName("fred"); + + RCustomer rCustomer = new RCustomer(); + rCustomer.setKey(key); + + BeanDescriptor rcustDesc = spiServer.getBeanDescriptor(RCustomer.class); + + Map idForJson = (Map)rcustDesc.getIdForJson(rCustomer); + assertEquals("comp",idForJson.get("company")); + assertEquals("fred",idForJson.get("name")); + assertEquals(2, idForJson.size()); + + RCustomerKey keyVal = (RCustomerKey)rcustDesc.convertIdFromJson(idForJson); + assertEquals("comp",keyVal.getCompany()); + assertEquals("fred",keyVal.getName()); + } private EntityBean entityBean(Object bean) { diff --git a/src/test/java/com/avaje/tests/model/basic/Country.java b/src/test/java/com/avaje/tests/model/basic/Country.java index 494dc5228..ad9fdb6a6 100644 --- a/src/test/java/com/avaje/tests/model/basic/Country.java +++ b/src/test/java/com/avaje/tests/model/basic/Country.java @@ -4,6 +4,7 @@ import com.avaje.ebean.annotation.CacheStrategy; import com.avaje.ebean.annotation.CacheTuning; import com.avaje.ebean.annotation.ChangeLog; import com.avaje.ebean.annotation.ChangeLogInsertMode; +import com.avaje.ebean.annotation.ReadAudit; import javax.persistence.Entity; import javax.persistence.Id; @@ -13,6 +14,7 @@ import javax.validation.constraints.Size; /** * Country entity bean. */ +@ReadAudit @ChangeLog(inserts = ChangeLogInsertMode.INCLUDE) @CacheStrategy(readOnly = true, warmingQuery = "order by name") @CacheTuning(maxSize = 500) diff --git a/src/test/java/com/avaje/tests/model/basic/EBasicChangeLog.java b/src/test/java/com/avaje/tests/model/basic/EBasicChangeLog.java index 65190e3c2..93332f3c4 100644 --- a/src/test/java/com/avaje/tests/model/basic/EBasicChangeLog.java +++ b/src/test/java/com/avaje/tests/model/basic/EBasicChangeLog.java @@ -1,6 +1,7 @@ package com.avaje.tests.model.basic; import com.avaje.ebean.annotation.ChangeLog; +import com.avaje.ebean.annotation.ReadAudit; import com.avaje.ebean.annotation.WhenCreated; import com.avaje.ebean.annotation.WhenModified; import com.avaje.ebean.annotation.WhoCreated; @@ -12,9 +13,7 @@ import javax.persistence.Version; import javax.validation.constraints.Size; import java.sql.Timestamp; -/** - * - */ +@ReadAudit @ChangeLog(updatesThatInclude = {"name","shortDescription"}) @Entity public class EBasicChangeLog { diff --git a/src/test/java/com/avaje/tests/query/TestIContains.java b/src/test/java/com/avaje/tests/query/TestIContains.java index ac79cb41c..c3c14c2c4 100644 --- a/src/test/java/com/avaje/tests/query/TestIContains.java +++ b/src/test/java/com/avaje/tests/query/TestIContains.java @@ -1,6 +1,8 @@ package com.avaje.tests.query; +import com.avaje.ebean.RawSql; +import com.avaje.ebean.RawSqlBuilder; import org.junit.Assert; import org.junit.Test; @@ -33,6 +35,14 @@ public class TestIContains extends BaseTestCase { Assert.assertTrue(generatedSql.contains(" t0.name ")); + Ebean.find(Customer.class).where().icontains("name", "Rob").findList(); + Ebean.find(Customer.class).where().icontains("name", "Rob").findList(); + Ebean.find(Customer.class).where().icontains("name", "Rob").findList(); + + String sql = "select id, status, name from o_customer where lower(name) like :name"; + RawSql parse = RawSqlBuilder.parse(sql).create(); + + Ebean.find(Customer.class).setRawSql(parse).setParameter("name", "Jim").findList(); } } diff --git a/src/test/java/com/avaje/tests/readaudit/TestReadAudit.java b/src/test/java/com/avaje/tests/readaudit/TestReadAudit.java new file mode 100644 index 000000000..2bbfe1774 --- /dev/null +++ b/src/test/java/com/avaje/tests/readaudit/TestReadAudit.java @@ -0,0 +1,381 @@ +package com.avaje.tests.readaudit; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.EbeanServerFactory; +import com.avaje.ebean.FutureList; +import com.avaje.ebean.QueryEachConsumer; +import com.avaje.ebean.cache.ServerCache; +import com.avaje.ebean.cache.ServerCacheStatistics; +import com.avaje.ebean.config.ServerConfig; +import com.avaje.ebean.event.readaudit.ReadAuditLogger; +import com.avaje.ebean.event.readaudit.ReadAuditPrepare; +import com.avaje.ebean.event.readaudit.ReadAuditQueryPlan; +import com.avaje.ebean.event.readaudit.ReadEvent; +import com.avaje.ebeaninternal.api.SpiEbeanServer; +import com.avaje.tests.model.basic.Country; +import com.avaje.tests.model.basic.EBasicChangeLog; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestReadAudit extends BaseTestCase { + + TDReadAuditPrepare readAuditPrepare = new TDReadAuditPrepare(true); + + TDReadAuditLogger readAuditLogger = new TDReadAuditLogger(true); + + SpiEbeanServer server; + + Long id1; + Long id2; + + @Before + public void setup() { + server = getServer(); + + EBasicChangeLog bean = new EBasicChangeLog(); + bean.setName("readAudito1"); + bean.setShortDescription("readAudit hello"); + server.save(bean); + id1 = bean.getId(); + + EBasicChangeLog bean2 = new EBasicChangeLog(); + bean2.setName("readAudito2"); + bean2.setShortDescription("readAudit hi"); + server.save(bean2); + id2 = bean2.getId(); + + Country ar = new Country(); + ar.setCode("AR"); + ar.setName("Argentina"); + server.save(ar); + } + + @After + public void shutdown() { + server.shutdown(true, false); + } + + @Test + public void test_findById() { + resetCounters(); + + EBasicChangeLog found = server.find(EBasicChangeLog.class, id1); + assertThat(found).isNotNull(); + assertThat(readAuditPrepare.count).isEqualTo(1); + assertThat(readAuditLogger.plans).hasSize(1); + assertThat(readAuditLogger.beans).hasSize(1); + assertThat(readAuditLogger.beans.get(0).getBeanType()).isEqualTo(EBasicChangeLog.class.getName()); + assertThat(readAuditLogger.beans.get(0).getId()).isEqualTo(id1); + } + + @Test + public void test_findById_usingL2Cache() { + resetCounters(); + + EBasicChangeLog found = server.find(EBasicChangeLog.class).setId(id1).setUseCache(true).findUnique(); + assertThat(found).isNotNull(); + assertThat(readAuditPrepare.count).isEqualTo(1); + assertThat(readAuditLogger.plans).hasSize(1); + assertThat(readAuditLogger.beans).hasSize(1); + assertThat(readAuditLogger.beans.get(0).getBeanType()).isEqualTo(EBasicChangeLog.class.getName()); + assertThat(readAuditLogger.beans.get(0).getId()).isEqualTo(id1); + + ServerCache beanCache = server.getServerCacheManager().getBeanCache(EBasicChangeLog.class); + ServerCacheStatistics statistics = beanCache.getStatistics(false); + assertThat(statistics.getSize()).isEqualTo(1); + assertThat(statistics.getHitCount()).isEqualTo(0); + + EBasicChangeLog found2 = server.find(EBasicChangeLog.class).setId(id1).setUseCache(true).findUnique(); + assertThat(found2).isNotNull(); + statistics = beanCache.getStatistics(false); + assertThat(statistics.getSize()).isEqualTo(1); + assertThat(statistics.getHitCount()).isEqualTo(1); + + assertThat(readAuditLogger.beans).hasSize(2); + } + + @Test + public void test_findById_usingL2Cache_sharedBean() { + resetCounters(); + + Country found = server.find(Country.class).setId("AR").setUseCache(true).findUnique(); + assertThat(found).isNotNull(); + assertThat(readAuditPrepare.count).isEqualTo(1); + assertThat(readAuditLogger.plans).hasSize(1); + assertThat(readAuditLogger.beans).hasSize(1); + assertThat(readAuditLogger.beans.get(0).getBeanType()).isEqualTo(Country.class.getName()); + assertThat(readAuditLogger.beans.get(0).getId()).isEqualTo("AR"); + + ServerCache beanCache = server.getServerCacheManager().getBeanCache(Country.class); + ServerCacheStatistics statistics = beanCache.getStatistics(false); + assertThat(statistics.getSize()).isEqualTo(1); + assertThat(statistics.getHitCount()).isEqualTo(0); + + Country found2 = server.find(Country.class).setId("AR").setUseCache(true).findUnique(); + assertThat(found2).isNotNull(); + statistics = beanCache.getStatistics(false); + assertThat(statistics.getSize()).isEqualTo(1); + assertThat(statistics.getHitCount()).isEqualTo(1); + + assertThat(readAuditLogger.beans).hasSize(2); + + Country ref = server.getReference(Country.class, "AR"); + assertThat(readAuditLogger.beans).hasSize(3); + assertThat(ref).isSameAs(found2); + } + + @Test + public void test_findList() { + + resetCounters(); + + List list = server.find(EBasicChangeLog.class) + .where().startsWith("shortDescription", "readAudit") + .findList(); + + assertThat(list).hasSize(2); + assertThat(readAuditPrepare.count).isEqualTo(1); + assertThat(readAuditLogger.plans).hasSize(1); + assertThat(readAuditLogger.many).hasSize(1); + assertThat(readAuditLogger.many.get(0).getBeanType()).isEqualTo(EBasicChangeLog.class.getName()); + assertThat(readAuditLogger.many.get(0).getIds()).contains(id1, id2); + + server.find(EBasicChangeLog.class) + .where().startsWith("shortDescription", "readAudit") + .findList(); + + assertThat(readAuditPrepare.count).isEqualTo(2); + assertThat(readAuditLogger.plans).hasSize(1); + assertThat(readAuditLogger.many).hasSize(2); + } + + @Test + public void test_findList_useL2Cache() { + + resetCounters(); + + List list = server.find(EBasicChangeLog.class) + .setUseQueryCache(true) + .where().startsWith("shortDescription", "readAudit") + .findList(); + + assertThat(list).hasSize(2); + assertThat(readAuditPrepare.count).isEqualTo(1); + assertThat(readAuditLogger.plans).hasSize(1); + assertThat(readAuditLogger.many).hasSize(1); + assertThat(readAuditLogger.many.get(0).getBeanType()).isEqualTo(EBasicChangeLog.class.getName()); + assertThat(readAuditLogger.many.get(0).getIds()).contains(id1, id2); + + ServerCache queryCache = server.getServerCacheManager().getQueryCache(EBasicChangeLog.class); + ServerCacheStatistics statistics = queryCache.getStatistics(false); + assertThat(statistics.getSize()).isEqualTo(1); + assertThat(statistics.getHitCount()).isEqualTo(0); + + server.find(EBasicChangeLog.class) + .setUseQueryCache(true) + .where().startsWith("shortDescription", "readAudit") + .findList(); + + statistics = queryCache.getStatistics(false); + assertThat(statistics.getSize()).isEqualTo(1); + assertThat(statistics.getHitCount()).isEqualTo(1); + + assertThat(readAuditPrepare.count).isEqualTo(2); + assertThat(readAuditLogger.plans).hasSize(1); + assertThat(readAuditLogger.many).hasSize(2); + } + + @Test + public void test_findFutureList() throws ExecutionException, InterruptedException { + + resetCounters(); + + FutureList futureList = server.find(EBasicChangeLog.class) + .where().startsWith("shortDescription", "readAudit") + .findFutureList(); + + List list = futureList.get(); + assertThat(list).hasSize(2); + assertThat(readAuditPrepare.count).isEqualTo(1); + assertThat(readAuditLogger.plans).hasSize(1); + assertThat(readAuditLogger.many).hasSize(1); + assertThat(readAuditLogger.many.get(0).getBeanType()).isEqualTo(EBasicChangeLog.class.getName()); + assertThat(readAuditLogger.many.get(0).getIds()).contains(id1, id2); + + server.find(EBasicChangeLog.class) + .where().startsWith("shortDescription", "readAudit") + .findList(); + + assertThat(readAuditPrepare.count).isEqualTo(2); + assertThat(readAuditLogger.plans).hasSize(1); + assertThat(readAuditLogger.many).hasSize(2); + } + + @Test + public void test_findSet() { + + resetCounters(); + + Set list = server.find(EBasicChangeLog.class) + .where().startsWith("shortDescription", "readAudit") + .findSet(); + + assertThat(list).hasSize(2); + assertThat(readAuditPrepare.count).isEqualTo(1); + assertThat(readAuditLogger.plans).hasSize(1); + assertThat(readAuditLogger.many).hasSize(1); + assertThat(readAuditLogger.many.get(0).getIds()).contains(id1, id2); + + server.find(EBasicChangeLog.class) + .where().startsWith("shortDescription", "readAudit") + .findSet(); + + assertThat(readAuditPrepare.count).isEqualTo(2); + assertThat(readAuditLogger.plans).hasSize(1); + assertThat(readAuditLogger.many).hasSize(2); + } + + @Test + public void test_findMap() { + + resetCounters(); + + Map list = server.find(EBasicChangeLog.class) + .where().startsWith("shortDescription", "readAudit") + .findMap(); + + assertThat(list).hasSize(2); + assertThat(readAuditPrepare.count).isEqualTo(1); + assertThat(readAuditLogger.plans).hasSize(1); + assertThat(readAuditLogger.many).hasSize(1); + assertThat(readAuditLogger.many.get(0).getIds()).contains(id1, id2); + + server.find(EBasicChangeLog.class) + .where().startsWith("shortDescription", "readAudit") + .findMap(); + + assertThat(readAuditPrepare.count).isEqualTo(2); + assertThat(readAuditLogger.plans).hasSize(1); + assertThat(readAuditLogger.many).hasSize(2); + } + + @Test + public void test_findEach() { + + resetCounters(); + + final AtomicInteger count = new AtomicInteger(); + server.find(EBasicChangeLog.class) + .where().startsWith("shortDescription", "readAudit") + .findEach(new QueryEachConsumer() { + @Override + public void accept(EBasicChangeLog bean) { + count.incrementAndGet(); + } + }); + + assertThat(count.get()).isEqualTo(2); + assertThat(readAuditPrepare.count).isEqualTo(1); + assertThat(readAuditLogger.plans).hasSize(1); + assertThat(readAuditLogger.many).hasSize(1); + assertThat(readAuditLogger.many.get(0).getIds()).contains(id1, id2); + + server.find(EBasicChangeLog.class) + .where().startsWith("shortDescription", "readAudit") + .findEach(new QueryEachConsumer() { + @Override + public void accept(EBasicChangeLog bean) { + count.incrementAndGet(); + } + }); + + assertThat(readAuditPrepare.count).isEqualTo(2); + assertThat(readAuditLogger.plans).hasSize(1); + assertThat(readAuditLogger.many).hasSize(2); + } + + 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(Country.class); + config.addClass(EBasicChangeLog.class); + + config.setReadAuditLogger(readAuditLogger); + config.setReadAuditPrepare(readAuditPrepare); + + return (SpiEbeanServer) EbeanServerFactory.create(config); + } + + private void resetCounters() { + readAuditLogger.resetCounters(); + readAuditPrepare.resetCounters(); + } + + class TDReadAuditPrepare implements ReadAuditPrepare { + + int count; + + TDReadAuditPrepare(boolean dummy) { + } + void resetCounters() { + count = 0; + } + + @Override + public void prepare(ReadEvent event) { + count++; + event.setUserId("appUser1"); + event.setUserIpAddress("1.1.1.1"); + event.getUserContext().put("some", "thing"); + } + } + + class TDReadAuditLogger implements ReadAuditLogger { + + List plans = new ArrayList(); + List beans = new ArrayList(); + List many = new ArrayList(); + + TDReadAuditLogger(boolean dummy) { + } + void resetCounters() { + plans.clear(); + beans.clear(); + many.clear(); + } + + @Override + public void queryPlan(ReadAuditQueryPlan queryPlan) { + plans.add(queryPlan); + } + + @Override + public void auditBean(ReadEvent readBean) { + beans.add(readBean); + } + + @Override + public void auditMany(ReadEvent readMany) { + many.add(readMany); + } + } + +} diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index b52efc14d..6f9ee9b15 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -22,6 +22,28 @@ + + log/readAuditQuery.log + + log/readAuditQuery.log.%d{yyyy-MM-dd} + 90 + + + %d{HH:mm:ss.SSS} %msg%n + + + + + log/readAudit.log + + log/readAudit.log.%d{yyyy-MM-dd} + 90 + + + %d{HH:mm:ss.SSS} %msg%n + + + log/changeLog.log @@ -33,6 +55,14 @@ + + + + + + + +