iterateStatistics();
+
+ /**
+ * Return true if profiling is enabled.
+ */
+ public boolean isProfiling();
+
+ /**
+ * Set to true to enable profiling.
+ *
+ * We rely on garbage collection to collect the profiling information. This
+ * means there is a unknown delay between when a query is executed and when
+ * we actually collect the usage profile information.
+ *
+ *
+ * Due to this garbage collection delay, when turning off profiling while
+ * the application is running you should consider calling
+ * collectUsageViaGC() BEFORE setProfiling(false). This hints to
+ * the JVM to perform garbage collection, and hopefully collects the
+ * profiling information.
+ *
+ */
+ public void setProfiling(boolean enable);
+
+ /**
+ * Return true if automatic query tuning is enabled.
+ */
+ public boolean isQueryTuning();
+
+ /**
+ * Set to true to enable automatic query tuning.
+ */
+ public void setQueryTuning(boolean enable);
+
+ /**
+ * This controls whether autoFetch is used when it has not been explicitly
+ * set on a query via {@link Query#setAutoFetch(boolean)}.
+ */
+ public AutofetchMode getMode();
+
+ /**
+ * Set the auto fetch mode used when a query has not had
+ * {@link Query#setAutoFetch(boolean)}.
+ */
+ public void setMode(AutofetchMode Mode);
+
+ /**
+ * Return the profiling rate (int between 0 and 100).
+ */
+ public double getProfilingRate();
+
+ /**
+ * Set the profiling rate (int between 0 and 100).
+ */
+ public void setProfilingRate(double rate);
+
+ /**
+ * Return the max number of queries profiled (per query point).
+ *
+ * The number of queries profiled is collected per query point. Once a query
+ * point has profiled this number of queries it does not profile any more.
+ *
+ */
+ public int getProfilingBase();
+
+ /**
+ * Set a max number of queries to profile per query point.
+ *
+ * This number should provide a level of confidence that no more profiling
+ * is required for this query point.
+ *
+ */
+ public void setProfilingBase(int profilingMax);
+
+ /**
+ * Return the minimum number of queries profiled before autoFetch will start
+ * automatically tuning the queries.
+ *
+ * This could be one which means start autoFetch tuning after the first
+ * profiling information is collected.
+ *
+ */
+ public int getProfilingMin();
+
+ /**
+ * Set the minimum number of queries profiled per query point before
+ * autoFetch will automatically tune the queries.
+ *
+ * Increasing this number will mean more profiling is collected before
+ * autoFetch starts tuning the query.
+ *
+ */
+ public void setProfilingMin(int autoFetchMinThreshold);
+
+ /**
+ * Fire a garbage collection (hint to the JVM). Assuming garbage collection
+ * fires this will gather the usage profiling information.
+ */
+ public String collectUsageViaGC(long waitMillis);
+
+ /**
+ * This will take the current profiling information and update the "tuned
+ * query detail".
+ *
+ * This is done periodically and can also be manually invoked.
+ *
+ *
+ * This returns a string summary of the updates that occurred.
+ *
+ */
+ public String updateTunedQueryInfo();
+
+ /**
+ * Called when a query thinks it should be automatically tuned by autoFetch.
+ *
+ * This internally checks that autoFetch is enabled, there is a "tuned query
+ * detail" to tune the query with and that the autoFetchMinThreshold has
+ * been reached.
+ *
+ *
+ * This will also determine if the query should be profiled.
+ *
+ */
+ public boolean tuneQuery(SpiQuery> query);
+
+ /**
+ * Collect query profiling information.
+ *
+ * This is for the original query as well as any subsequent lazy loading
+ * queries that are required as the object graph is traversed.
+ *
+ *
+ * @param node
+ * the node path in the object graph.
+ * @param beans
+ * the number of beans loaded by the query.
+ * @param micros
+ * the query executing time in microseconds
+ */
+ public void collectQueryInfo(ObjectGraphNode node, int beans, int micros);
+
+
+ /**
+ * Return the number of queries tuned by AutoFetch.
+ */
+ public int getTotalTunedQueryCount();
+
+ /**
+ * Return the size of the TuneQuery map.
+ */
+ public int getTotalTunedQuerySize();
+
+ /**
+ * Return the size of the profile map.
+ */
+ public int getTotalProfileSize();
+}
diff --git a/src/main/java/com/avaje/ebeaninternal/server/autofetch/AutoFetchManagerFactory.java b/src/main/java/com/avaje/ebeaninternal/server/autofetch/AutoFetchManagerFactory.java
new file mode 100644
index 000000000..c9b7ce31f
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/autofetch/AutoFetchManagerFactory.java
@@ -0,0 +1,97 @@
+package com.avaje.ebeaninternal.server.autofetch;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.ObjectInputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.persistence.PersistenceException;
+
+import com.avaje.ebean.config.ServerConfig;
+import com.avaje.ebean.config.GlobalProperties;
+import com.avaje.ebeaninternal.api.SpiEbeanServer;
+import com.avaje.ebeaninternal.server.resource.ResourceManager;
+
+public class AutoFetchManagerFactory {
+
+ private static final Logger logger = Logger.getLogger(AutoFetchManagerFactory.class.getName());
+
+
+ public static AutoFetchManager create(SpiEbeanServer server, ServerConfig serverConfig, ResourceManager resourceManager) {
+
+ AutoFetchManagerFactory me = new AutoFetchManagerFactory();
+ return me.createAutoFetchManager(server, serverConfig, resourceManager);
+ }
+
+ private AutoFetchManager createAutoFetchManager(SpiEbeanServer server, ServerConfig serverConfig, ResourceManager resourceManager){
+
+ AutoFetchManager manager = createAutoFetchManager(server.getName(), resourceManager);
+ manager.setOwner(server, serverConfig);
+
+ return manager;
+ }
+
+ private AutoFetchManager createAutoFetchManager(String serverName, ResourceManager resourceManager) {
+
+ File autoFetchFile = getAutoFetchFile(serverName, resourceManager);
+
+ AutoFetchManager autoFetchManager = null;
+
+ boolean readFile = GlobalProperties.getBoolean("autofetch.readfromfile", true);
+ if (readFile) {
+ autoFetchManager = deserializeAutoFetch(autoFetchFile);
+ }
+
+ if (autoFetchManager == null) {
+ // not deserialized from file so create as empty
+ // It will be populated automatically by querying the
+ // database meta data
+ autoFetchManager = new DefaultAutoFetchManager(autoFetchFile.getAbsolutePath());
+ }
+
+ return autoFetchManager;
+ }
+
+ private AutoFetchManager deserializeAutoFetch(File autoFetchFile) {
+ try {
+
+ if (!autoFetchFile.exists()) {
+ return null;
+ }
+ FileInputStream fi = new FileInputStream(autoFetchFile);
+ ObjectInputStream ois = new ObjectInputStream(fi);
+ AutoFetchManager profListener = (AutoFetchManager) ois.readObject();
+
+ logger.info("AutoFetch deserialized from file ["+autoFetchFile.getAbsolutePath()+"]");
+
+ return profListener;
+
+ } catch (Exception ex) {
+ logger.log(Level.SEVERE, "Error loading autofetch file "+autoFetchFile.getAbsolutePath(), ex);
+ return null;
+ }
+ }
+
+ /**
+ * Return the file name of the autoFetch meta data.
+ */
+ private File getAutoFetchFile(String serverName, ResourceManager resourceManager) {
+
+ String fileName = ".ebean."+serverName+".autofetch";
+
+ File dir = resourceManager.getAutofetchDirectory();
+
+ if (!dir.exists()) {
+ // automatically create the directory if it does not exist.
+ // this is probably a fairly reasonable thing to do
+ if (!dir.mkdirs()) {
+ String m = "Unable to create directory [" + dir + "] for autofetch file ["+ fileName + "]";
+ throw new PersistenceException(m);
+ }
+ }
+
+ return new File(dir, fileName);
+ }
+
+}
diff --git a/src/main/java/com/avaje/ebeaninternal/server/autofetch/DefaultAutoFetchManager.java b/src/main/java/com/avaje/ebeaninternal/server/autofetch/DefaultAutoFetchManager.java
new file mode 100644
index 000000000..698f05370
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/autofetch/DefaultAutoFetchManager.java
@@ -0,0 +1,582 @@
+package com.avaje.ebeaninternal.server.autofetch;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
+
+import javax.persistence.PersistenceException;
+
+import com.avaje.ebean.bean.CallStack;
+import com.avaje.ebean.bean.NodeUsageCollector;
+import com.avaje.ebean.bean.ObjectGraphNode;
+import com.avaje.ebean.bean.ObjectGraphOrigin;
+import com.avaje.ebean.config.AutofetchConfig;
+import com.avaje.ebean.config.AutofetchMode;
+import com.avaje.ebean.config.ServerConfig;
+import com.avaje.ebeaninternal.api.ClassUtil;
+import com.avaje.ebeaninternal.api.SpiEbeanServer;
+import com.avaje.ebeaninternal.api.SpiQuery;
+import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
+import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetail;
+
+/**
+ * The manager of all the usage/query statistics as well as the tuned fetch
+ * information.
+ */
+public class DefaultAutoFetchManager implements AutoFetchManager, Serializable {
+
+ private static final long serialVersionUID = -6826119882781771722L;
+
+ private final String statisticsMonitor = new String();
+
+ private final String fileName;
+
+ /**
+ * Map of the usage and query statistics gathered.
+ */
+ private Map statisticsMap = new ConcurrentHashMap();
+
+ /**
+ * Map of the tuned query details per profile query point.
+ */
+ private Map tunedQueryInfoMap = new ConcurrentHashMap();
+
+ private transient long defaultGarbageCollectionWait = 100;
+
+ /**
+ * Left without synchronized for now.
+ */
+ private transient int tunedQueryCount;
+
+ /**
+ * Converted from a 0-100 int to a double. Effectively a percentage rate at
+ * which to collect profiling information.
+ */
+ private transient double profilingRate = 0.1d;
+
+ private transient int profilingBase = 10;
+
+ private transient int profilingMin = 1;
+
+ private transient boolean profiling;
+
+ private transient boolean queryTuning;
+
+ private transient boolean queryTuningAddVersion;
+
+ private transient AutofetchMode mode;
+
+ private transient boolean useFileLogging;
+
+ /**
+ * Server that owns this Profile Listener.
+ */
+ private transient SpiEbeanServer server;
+
+ /**
+ * The logger.
+ */
+ private transient DefaultAutoFetchManagerLogging logging;
+
+ public DefaultAutoFetchManager(String fileName) {
+ this.fileName = fileName;
+ }
+
+ /**
+ * Set up this profile listener before it is active.
+ */
+ public void setOwner(SpiEbeanServer server, ServerConfig serverConfig) {
+ this.server = server;
+ this.logging = new DefaultAutoFetchManagerLogging(serverConfig, this);
+
+ AutofetchConfig autofetchConfig = serverConfig.getAutofetchConfig();
+
+ useFileLogging = autofetchConfig.isUseFileLogging();
+ queryTuning = autofetchConfig.isQueryTuning();
+ queryTuningAddVersion = autofetchConfig.isQueryTuningAddVersion();
+ profiling = autofetchConfig.isProfiling();
+ profilingMin = autofetchConfig.getProfilingMin();
+ profilingBase = autofetchConfig.getProfilingBase();
+
+ setProfilingRate(autofetchConfig.getProfilingRate());
+
+
+ defaultGarbageCollectionWait = (long) autofetchConfig.getGarbageCollectionWait();
+
+ // determine the mode to use when Query.setAutoFetch() was
+ // not explicitly set
+ mode = autofetchConfig.getMode();
+
+ if (profiling || queryTuning) {
+ // log the guts of the autoFetch setup
+ String msg = "AutoFetch queryTuning[" + queryTuning + "] profiling[" + profiling
+ + "] mode[" + mode + "] profiling rate[" + profilingRate
+ + "] min[" + profilingMin + "] base[" + profilingBase + "]";
+ logging.logToJavaLogger(msg);
+ }
+ }
+
+
+
+ public void clearQueryStatistics() {
+ server.clearQueryStatistics();
+ }
+
+ /**
+ * Return the number of queries tuned by AutoFetch.
+ */
+ public int getTotalTunedQueryCount(){
+ return tunedQueryCount;
+ }
+
+ /**
+ * Return the size of the TuneQuery map.
+ */
+ public int getTotalTunedQuerySize(){
+ return tunedQueryInfoMap.size();
+ }
+
+ /**
+ * Return the size of the profile map.
+ */
+ public int getTotalProfileSize(){
+ return statisticsMap.size();
+ }
+
+ public int clearTunedQueryInfo() {
+
+ // reset the rough count as well
+ tunedQueryCount = 0;
+
+ // clear the map...
+ int size = tunedQueryInfoMap.size();
+ tunedQueryInfoMap.clear();
+ return size;
+ }
+
+ public int clearProfilingInfo() {
+ int size = statisticsMap.size();
+ statisticsMap.clear();
+ return size;
+ }
+
+
+ public void serialize() {
+
+ File autoFetchFile = new File(fileName);
+
+ try {
+ FileOutputStream fout = new FileOutputStream(autoFetchFile);
+
+ ObjectOutputStream oout = new ObjectOutputStream(fout);
+ oout.writeObject(this);
+ oout.flush();
+ oout.close();
+
+ } catch (Exception e) {
+ String msg = "Error serializing autofetch file";
+ logging.logError(Level.SEVERE, msg, e);
+ }
+ }
+
+ /**
+ * Return the current Tuned query info for a given origin key.
+ */
+ public TunedQueryInfo getTunedQueryInfo(String originKey) {
+ return tunedQueryInfoMap.get(originKey);
+ }
+
+ /**
+ * Return the current Statistics for a given originKey key.
+ */
+ public Statistics getStatistics(String originKey) {
+ return statisticsMap.get(originKey);
+ }
+
+ public Iterator iterateTunedQueryInfo() {
+ return tunedQueryInfoMap.values().iterator();
+ }
+
+ public Iterator iterateStatistics() {
+ return statisticsMap.values().iterator();
+ }
+
+ public boolean isProfiling() {
+ return profiling;
+ }
+
+ /**
+ * When the application is running, BEFORE turning off profiling you
+ * probably should call collectUsageViaGC() as there is a delay (waiting for
+ * garbage collection) collecting usage profiling information.
+ */
+ public void setProfiling(boolean profiling) {
+ this.profiling = profiling;
+ }
+
+ public boolean isQueryTuning() {
+ return queryTuning;
+ }
+
+ public void setQueryTuning(boolean queryTuning) {
+ this.queryTuning = queryTuning;
+ }
+
+ public double getProfilingRate() {
+ return profilingRate;
+ }
+
+ public AutofetchMode getMode() {
+ return mode;
+ }
+
+ public void setMode(AutofetchMode mode) {
+ this.mode = mode;
+ }
+
+ public void setProfilingRate(double rate) {
+ if (rate < 0) {
+ rate = 0d;
+ } else if (rate > 1) {
+ rate = 1d;
+ }
+ profilingRate = rate;
+ }
+
+ public int getProfilingBase() {
+ return profilingBase;
+ }
+
+ public void setProfilingBase(int profilingBase) {
+ this.profilingBase = profilingBase;
+ }
+
+ public int getProfilingMin() {
+ return profilingMin;
+ }
+
+ public void setProfilingMin(int profilingMin) {
+ this.profilingMin = profilingMin;
+ }
+
+ /**
+ * Shutdown the listener.
+ *
+ * We should try to collect the usage statistics by calling a System.gc().
+ * This is necessary for use with short lived applications where garbage
+ * collection may not otherwise occur at all.
+ *
+ */
+ public void shutdown() {
+ if (useFileLogging) {
+ collectUsageViaGC(-1);
+ serialize();
+ }
+ }
+
+ /**
+ * Ask for a System.gc() so that we gather node usage information.
+ *
+ * Really only want to do this sparingly but useful just prior to shutdown
+ * for short run application where garbage collection may otherwise not
+ * occur at all.
+ *
+ *
+ * waitMillis will do a thread sleep to give the garbage collection a little
+ * time to do its thing assuming we are shutting down the VM.
+ *
+ *
+ * If waitMillis is -1 then the defaultGarbageCollectionWait is used which
+ * defaults to 100 milliseconds.
+ *
+ */
+ public String collectUsageViaGC(long waitMillis) {
+ System.gc();
+ try {
+ if (waitMillis < 0) {
+ waitMillis = defaultGarbageCollectionWait;
+ }
+ Thread.sleep(waitMillis);
+ } catch (InterruptedException e) {
+ String msg = "Error while sleeping after System.gc() request.";
+ logging.logError(Level.SEVERE, msg, e);
+ return msg;
+ }
+ return updateTunedQueryInfo();
+ }
+
+ /**
+ * Update the tuned fetch plans from the current usage information.
+ */
+ public String updateTunedQueryInfo() {
+
+ if (!profiling) {
+ // we are not collecting any profiling information at
+ // the moment so don't try updating the tuned query plans.
+ return "Not profiling";
+ }
+
+ synchronized (statisticsMonitor) {
+
+ Counters counters = new Counters();
+
+ Iterator it = statisticsMap.values().iterator();
+ while (it.hasNext()) {
+ Statistics queryPointStatistics = it.next();
+ if (!queryPointStatistics.hasUsage()){
+ // no usage statistics collected yet...
+ counters.incrementNoUsage();
+ } else {
+ updateTunedQueryFromUsage(counters, queryPointStatistics);
+ }
+ }
+
+ String summaryInfo = counters.toString();
+
+ if (counters.isInteresting()){
+ // only log it if its interesting
+ logging.logSummary(summaryInfo);
+ }
+
+ return summaryInfo;
+ }
+ }
+
+ private static class Counters {
+
+ int newPlan;
+ int modified;
+ int unchanged;
+ int noUsage;
+
+ void incrementNoUsage(){
+ noUsage++;
+ }
+ void incrementNew(){
+ newPlan++;
+ }
+ void incrementModified(){
+ modified++;
+ }
+ void incrementUnchanged(){
+ unchanged++;
+ }
+ boolean isInteresting() {
+ return newPlan > 0 || modified > 0;
+ }
+ public String toString() {
+ return "new["+newPlan+"] modified["+modified+"] unchanged["+unchanged+"] nousage["+noUsage+"]";
+ }
+ }
+
+ private void updateTunedQueryFromUsage(Counters counters, Statistics statistics) {
+
+ ObjectGraphOrigin queryPoint = statistics.getOrigin();
+ String beanType = queryPoint.getBeanType();
+
+ try {
+ Class> beanClass = ClassUtil.forName(beanType, this.getClass());
+ BeanDescriptor> beanDescriptor = server.getBeanDescriptor(beanClass);
+ if (beanDescriptor == null){
+ // previously was an entity but not longer
+
+ } else {
+ // Determine the fetch plan from the latest statistics.
+ // Use this to compare with current "tuned fetch plan".
+ OrmQueryDetail newFetchDetail = statistics.buildTunedFetch(beanDescriptor);
+
+ // get the current tuned fetch info...
+ TunedQueryInfo currentFetch = tunedQueryInfoMap.get(queryPoint.getKey());
+
+ if (currentFetch == null) {
+ // its a new fetch plan, add it.
+ counters.incrementNew();
+
+ currentFetch = statistics.createTunedFetch(newFetchDetail);
+ logging.logNew(currentFetch);
+ tunedQueryInfoMap.put(queryPoint.getKey(), currentFetch);
+
+ } else if (!currentFetch.isSame(newFetchDetail)) {
+ // the fetch plan has changed, update it.
+ counters.incrementModified();
+
+ logging.logChanged(currentFetch, newFetchDetail);
+ currentFetch.setTunedDetail(newFetchDetail);
+
+ } else {
+ // the fetch plan has not changed...
+ counters.incrementUnchanged();
+ }
+
+ currentFetch.setProfileCount(statistics.getCounter());
+ }
+
+ } catch (ClassNotFoundException e) {
+ // expected after renaming/moving an entity bean
+ String msg = e.toString()+" updating autoFetch tuned query for " + beanType
+ +". It isLikely this bean has been renamed or moved";
+ logging.logError(Level.INFO, msg, null);
+ statisticsMap.remove(statistics.getOrigin().getKey());
+ }
+ }
+
+ /**
+ * Return true if we should try to use autoFetch for this query.
+ */
+ private boolean useAutoFetch(SpiQuery> query) {
+
+ if (query.isLoadBeanCache()){
+ // when loading the cache don't tune the query
+ // as we want full objects loaded into the cache
+ return false;
+ }
+
+ Boolean autoFetch = query.isAutofetch();
+ if (autoFetch != null) {
+ // explicitly set...
+ return autoFetch.booleanValue();
+
+ } else {
+ // determine using implicit mode...
+ switch (mode) {
+ case DEFAULT_ON:
+ return true;
+
+ case DEFAULT_OFF:
+ return false;
+
+ case DEFAULT_ONIFEMPTY:
+ return query.isDetailEmpty();
+
+ default:
+ throw new PersistenceException("Invalid autoFetchMode " + mode);
+ }
+ }
+ }
+
+ /**
+ * Auto tune the query and enable profiling.
+ */
+ public boolean tuneQuery(SpiQuery> query) {
+
+ if (!queryTuning && !profiling) {
+ return false;
+ }
+
+ if (!useAutoFetch(query)) {
+ // not using autoFetch for this query
+ return false;
+ }
+
+ ObjectGraphNode parentAutoFetchNode = query.getParentNode();
+ if (parentAutoFetchNode != null) {
+ // This is a +lazy/+query query with profiling on.
+ // We continue to collect the profiling information.
+ query.setAutoFetchManager(this);
+ return true;
+ }
+
+ // create a query point to identify the query
+ CallStack stack = server.createCallStack();
+ ObjectGraphNode origin = query.setOrigin(stack);
+
+ // get current "tuned fetch" for this query point
+ TunedQueryInfo tunedFetch = tunedQueryInfoMap.get(origin.getOriginQueryPoint().getKey());
+
+ // get the number of times we have collected profiling information
+ int profileCount = tunedFetch == null ? 0 : tunedFetch.getProfileCount();
+
+ if (profiling) {
+ // we want more profiling information?
+ if (tunedFetch == null) {
+ query.setAutoFetchManager(this);
+
+ } else if (profileCount < profilingBase) {
+ query.setAutoFetchManager(this);
+
+ } else if (tunedFetch.isPercentageProfile(profilingRate)) {
+ query.setAutoFetchManager(this);
+ }
+ }
+
+ if (queryTuning) {
+ if (tunedFetch != null && profileCount >= profilingMin) {
+ // deemed to have enough profiling
+ // information for automatic tuning
+ if (tunedFetch.autoFetchTune(query)){
+ // tunedQueryCount++ not thread-safe, could use AtomicInteger.
+ // But I'm happy if this statistic is a little wrong
+ // and this is a VERY HOT method
+ tunedQueryCount++;
+ }
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Gather query execution statistics. This could either be the originating
+ * query in which case the parentNode will be null, or a lazy loading query
+ * resulting from traversal of the object graph.
+ */
+ public void collectQueryInfo(ObjectGraphNode node, int beans, int micros) {
+
+ if (node != null){
+ ObjectGraphOrigin origin = node.getOriginQueryPoint();
+ if (origin != null){
+ Statistics stats = getQueryPointStats(origin);
+ stats.collectQueryInfo(node, beans, micros);
+ }
+ }
+ }
+
+ /**
+ * Collect usage statistics from a node in the object graph.
+ *
+ * This is sent to use from a EntityBeanIntercept when the finalise method
+ * is called on the bean.
+ *
+ */
+ public void collectNodeUsage(NodeUsageCollector usageCollector) {
+
+ ObjectGraphOrigin origin = usageCollector.getNode().getOriginQueryPoint();
+
+ Statistics stats = getQueryPointStats(origin);
+
+ if (logging.isTraceUsageCollection()){
+ System.out.println("... NodeUsageCollector "+usageCollector);
+ }
+
+ stats.collectUsageInfo(usageCollector);
+
+ if (logging.isTraceUsageCollection()){
+ System.out.println("stats\n"+stats);
+ }
+ }
+
+ private Statistics getQueryPointStats(ObjectGraphOrigin originQueryPoint) {
+ synchronized (statisticsMonitor) {
+ Statistics stats = statisticsMap.get(originQueryPoint.getKey());
+ if (stats == null) {
+ stats = new Statistics(originQueryPoint, queryTuningAddVersion);
+ statisticsMap.put(originQueryPoint.getKey(), stats);
+ }
+ return stats;
+ }
+ }
+
+ public String toString() {
+ synchronized (statisticsMonitor) {
+ return statisticsMap.values().toString();
+ }
+ }
+
+
+}
diff --git a/src/main/java/com/avaje/ebeaninternal/server/autofetch/DefaultAutoFetchManagerLogging.java b/src/main/java/com/avaje/ebeaninternal/server/autofetch/DefaultAutoFetchManagerLogging.java
new file mode 100644
index 000000000..f788ad303
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/autofetch/DefaultAutoFetchManagerLogging.java
@@ -0,0 +1,112 @@
+package com.avaje.ebeaninternal.server.autofetch;
+
+import java.util.logging.Level;
+
+import java.util.logging.Logger;
+
+import com.avaje.ebean.config.AutofetchConfig;
+import com.avaje.ebean.config.GlobalProperties;
+import com.avaje.ebean.config.ServerConfig;
+import com.avaje.ebeaninternal.server.lib.BackgroundThread;
+import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetail;
+import com.avaje.ebeaninternal.server.transaction.log.SimpleLogger;
+
+/**
+ * Handles the logging aspects for the DefaultAutoFetchListener.
+ *
+ * Note that java util logging loggers generally should not be serialised and
+ * that is one of the main reasons for pulling out the logging to this class.
+ *
+ */
+public class DefaultAutoFetchManagerLogging {
+
+ private static final Logger logger = Logger.getLogger(DefaultAutoFetchManagerLogging.class.getName());
+
+ private final SimpleLogger fileLogger;
+
+ private final DefaultAutoFetchManager manager;
+
+ private final boolean useFileLogger;
+
+ private final boolean traceUsageCollection;
+
+ public DefaultAutoFetchManagerLogging(ServerConfig serverConfig, DefaultAutoFetchManager profileListener) {
+
+ this.manager = profileListener;
+
+ AutofetchConfig autofetchConfig = serverConfig.getAutofetchConfig();
+
+ traceUsageCollection = GlobalProperties.getBoolean("ebean.autofetch.traceUsageCollection", false);
+ useFileLogger = autofetchConfig.isUseFileLogging();
+
+ if (!useFileLogger) {
+ fileLogger = null;
+
+ } else {
+ // a separate log file just like the transaction logging
+ // for putting the profiling log messages. The benefit is that
+ // this doesn't pollute the main log with heaps of messages.
+ String baseDir = serverConfig.getLoggingDirectoryWithEval();
+ fileLogger = new SimpleLogger(baseDir, "autofetch", true, "csv");
+ }
+
+ int updateFreqInSecs = autofetchConfig.getProfileUpdateFrequency();
+
+ BackgroundThread.add(updateFreqInSecs, new UpdateProfile());
+ }
+
+ private final class UpdateProfile implements Runnable {
+ public void run() {
+ manager.updateTunedQueryInfo();
+ }
+ }
+
+ public void logError(Level level, String msg, Throwable e) {
+ if (useFileLogger) {
+ String errMsg = e == null ? "" : e.getMessage();
+ fileLogger.log("\"Error\",\"" + msg+" "+errMsg+"\",,,,");
+ }
+ logger.log(level, msg, e);
+ }
+
+ public void logToJavaLogger(String msg) {
+ logger.info(msg);
+ }
+
+ public void logSummary(String summaryInfo) {
+
+ String msg = "\"Summary\",\""+summaryInfo+"\",,,,";
+
+ if (useFileLogger) {
+ fileLogger.log(msg);
+ }
+ logger.fine(msg);
+ }
+
+ public void logChanged(TunedQueryInfo tunedFetch, OrmQueryDetail newQueryDetail) {
+
+ String msg = tunedFetch.getLogOutput(newQueryDetail);
+
+ if (useFileLogger) {
+ fileLogger.log(msg);
+ } else {
+ logger.fine(msg);
+ }
+ }
+
+ public void logNew(TunedQueryInfo tunedFetch) {
+
+ String msg = tunedFetch.getLogOutput(null);
+
+ if (useFileLogger) {
+ fileLogger.log(msg);
+ } else {
+ logger.fine(msg);
+ }
+ }
+
+ public boolean isTraceUsageCollection() {
+ return traceUsageCollection;
+ }
+
+}
diff --git a/src/main/java/com/avaje/ebeaninternal/server/autofetch/Statistics.java b/src/main/java/com/avaje/ebeaninternal/server/autofetch/Statistics.java
new file mode 100644
index 000000000..7aa771f6d
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/autofetch/Statistics.java
@@ -0,0 +1,207 @@
+package com.avaje.ebeaninternal.server.autofetch;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.avaje.ebean.bean.NodeUsageCollector;
+import com.avaje.ebean.bean.ObjectGraphNode;
+import com.avaje.ebean.bean.ObjectGraphOrigin;
+import com.avaje.ebean.meta.MetaAutoFetchStatistic;
+import com.avaje.ebean.meta.MetaAutoFetchStatistic.NodeUsageStats;
+import com.avaje.ebean.meta.MetaAutoFetchStatistic.QueryStats;
+import com.avaje.ebean.text.PathProperties;
+import com.avaje.ebean.text.PathProperties.Props;
+import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
+import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetail;
+
+public class Statistics implements Serializable {
+
+
+ private static final long serialVersionUID = -5586783791097230766L;
+
+ private final ObjectGraphOrigin origin;
+
+ private final boolean queryTuningAddVersion;
+
+ private int counter;
+
+ private Map queryStatsMap = new LinkedHashMap();
+
+ private Map nodeUsageMap = new LinkedHashMap();
+
+ private final String monitor = new String();
+
+ public Statistics(ObjectGraphOrigin origin, boolean queryTuningAddVersion) {
+ this.origin = origin;
+ this.queryTuningAddVersion = queryTuningAddVersion;
+ }
+
+ public ObjectGraphOrigin getOrigin() {
+ return origin;
+ }
+
+ public TunedQueryInfo createTunedFetch(OrmQueryDetail newFetchDetail) {
+ synchronized (monitor) {
+ // NB: create a copy of queryPoint allowing garbage
+ // collection of source...
+ return new TunedQueryInfo(origin, newFetchDetail, counter);
+ }
+ }
+
+ public MetaAutoFetchStatistic createPublicMeta() {
+
+ synchronized (monitor) {
+
+ StatisticsQuery[] sourceQueryStats = queryStatsMap.values().toArray(new StatisticsQuery[queryStatsMap.size()]);
+ List destQueryStats = new ArrayList(sourceQueryStats.length);
+
+ // copy the query statistics
+ for (int i = 0; i < sourceQueryStats.length; i++) {
+ destQueryStats.add(sourceQueryStats[i].createPublicMeta());
+ }
+
+ StatisticsNodeUsage[] sourceNodeUsage = nodeUsageMap.values().toArray(new StatisticsNodeUsage[nodeUsageMap.size()]);
+ List destNodeUsage = new ArrayList(sourceNodeUsage.length);
+
+ // copy the node usage statistics
+ for (int i = 0; i < sourceNodeUsage.length; i++) {
+ destNodeUsage.add(sourceNodeUsage[i].createPublicMeta());
+ }
+
+ return new MetaAutoFetchStatistic(origin, counter, destQueryStats, destNodeUsage);
+ }
+ }
+
+ /**
+ * Return the number of times the root query has executed.
+ *
+ * This tells us how much profiling we have done for this query.
+ * For example, after 100 times we may stop collecting more profiling info.
+ *
+ */
+ public int getCounter() {
+ return counter;
+ }
+
+ /**
+ * Return true if this has usage statistics.
+ */
+ public boolean hasUsage() {
+ synchronized (monitor) {
+ return !nodeUsageMap.isEmpty();
+ }
+ }
+
+ public OrmQueryDetail buildTunedFetch(BeanDescriptor> rootDesc){
+
+ synchronized (monitor) {
+ if (nodeUsageMap.isEmpty()){
+ return null;
+ }
+
+ PathProperties pathProps = new PathProperties();
+
+ Iterator it = nodeUsageMap.values().iterator();
+ while (it.hasNext()) {
+ StatisticsNodeUsage statsNode = it.next();
+ statsNode.buildTunedFetch(pathProps, rootDesc);
+ }
+
+ OrmQueryDetail detail = new OrmQueryDetail();
+
+ Collection pathProperties = pathProps.getPathProps();
+ for (Props props : pathProperties) {
+ if (!props.isEmpty()){
+ detail.addFetch(props.getPath(), props.getPropertiesAsString(), null);
+ }
+ }
+
+ detail.sortFetchPaths(rootDesc);
+ return detail;
+ }
+ }
+
+
+ public void collectQueryInfo(ObjectGraphNode node, int beansLoaded, int micros) {
+
+ synchronized (monitor) {
+ String key = node.getPath();
+ if (key == null){
+ key = "";
+ // this is basically the number of times the root query
+ // has executed which gives us an indication of how
+ // much profiling information we have gathered.
+ counter++;
+ }
+
+ StatisticsQuery stats = queryStatsMap.get(key);
+ if (stats == null){
+ stats = new StatisticsQuery(key);
+ queryStatsMap.put(key, stats);
+ }
+ stats.add(beansLoaded, micros);
+ }
+ }
+
+
+ /**
+ * Collect the usage information for from a instance for this node.
+ */
+ public void collectUsageInfo(NodeUsageCollector profile) {
+
+ if (profile.isEmpty()){
+ // no usage was collected
+ } else {
+ ObjectGraphNode node = profile.getNode();
+
+ StatisticsNodeUsage nodeStats = getNodeStats(node.getPath());
+ nodeStats.publish(profile);
+ }
+ }
+
+ private StatisticsNodeUsage getNodeStats(String path) {
+
+ synchronized (monitor) {
+ StatisticsNodeUsage nodeStats = nodeUsageMap.get(path);
+ if (nodeStats == null) {
+ nodeStats = new StatisticsNodeUsage(path, queryTuningAddVersion);
+ nodeUsageMap.put(path, nodeStats);
+ }
+ return nodeStats;
+ }
+ }
+
+ public String getUsageDebug() {
+ synchronized (monitor) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("root[").append(origin.getBeanType()).append("] ");
+ for (StatisticsNodeUsage node : nodeUsageMap.values()) {
+ sb.append(node.toString()).append("\n");
+ }
+ return sb.toString();
+ }
+ }
+
+ public String getQueryStatDebug() {
+ synchronized (monitor) {
+ StringBuilder sb = new StringBuilder();
+ for (StatisticsQuery queryStat : queryStatsMap.values()) {
+ sb.append(queryStat.toString()).append("\n");
+ }
+ return sb.toString();
+ }
+ }
+
+ public String toString() {
+
+ synchronized (monitor) {
+ return getUsageDebug();
+ }
+ }
+
+}
diff --git a/src/main/java/com/avaje/ebeaninternal/server/autofetch/StatisticsNodeUsage.java b/src/main/java/com/avaje/ebeaninternal/server/autofetch/StatisticsNodeUsage.java
new file mode 100644
index 000000000..05741bae5
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/autofetch/StatisticsNodeUsage.java
@@ -0,0 +1,123 @@
+package com.avaje.ebeaninternal.server.autofetch;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import com.avaje.ebean.bean.NodeUsageCollector;
+import com.avaje.ebean.meta.MetaAutoFetchStatistic.NodeUsageStats;
+import com.avaje.ebean.text.PathProperties;
+import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
+import com.avaje.ebeaninternal.server.deploy.BeanProperty;
+import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssoc;
+import com.avaje.ebeaninternal.server.el.ElPropertyValue;
+import com.avaje.ebeaninternal.server.query.SplitName;
+
+/**
+ * Collects usages statistics for a given node in the object graph.
+ */
+public class StatisticsNodeUsage implements Serializable {
+
+ private static final long serialVersionUID = -1663951463963779547L;
+
+ private static final Logger logger = Logger.getLogger(StatisticsNodeUsage.class.getName());
+
+ private final String monitor = new String();
+
+ private final String path;
+
+ private final boolean queryTuningAddVersion;
+
+ private int profileCount;
+
+ private int profileUsedCount;
+
+ private boolean modified;
+
+ private Set aggregateUsed = new LinkedHashSet();
+
+ public StatisticsNodeUsage(String path, boolean queryTuningAddVersion) {
+ this.path = path;
+ this.queryTuningAddVersion = queryTuningAddVersion;
+ }
+
+ public NodeUsageStats createPublicMeta() {
+ synchronized(monitor){
+ String[] usedProps = aggregateUsed.toArray(new String[aggregateUsed.size()]);
+ return new NodeUsageStats(path, profileCount, profileUsedCount, usedProps);
+ }
+ }
+
+ public void buildTunedFetch(PathProperties pathProps, BeanDescriptor> rootDesc) {
+
+ synchronized(monitor){
+
+ BeanDescriptor> desc = rootDesc;
+ if (path != null){
+ ElPropertyValue elGetValue = rootDesc.getElGetValue(path);
+ if (elGetValue == null){
+ desc = null;
+ logger.warning("Autofetch: Can't find join for path["+path+"] for "+rootDesc.getName());
+
+ } else {
+ BeanProperty beanProperty = elGetValue.getBeanProperty();
+ if (beanProperty instanceof BeanPropertyAssoc>){
+ desc = ((BeanPropertyAssoc>) beanProperty).getTargetDescriptor();
+ }
+ }
+ }
+
+ for (String propName : aggregateUsed) {
+ BeanProperty beanProp = desc.getBeanPropertyFromPath(propName);
+ if (beanProp == null){
+ logger.warning("Autofetch: Can't find property["+propName+"] for "+desc.getName());
+
+ } else {
+ if (beanProp instanceof BeanPropertyAssoc>){
+ BeanPropertyAssoc> assocProp = (BeanPropertyAssoc>)beanProp;
+ String targetIdProp = assocProp.getTargetIdProperty();
+ String manyPath = SplitName.add(path, assocProp.getName());
+ pathProps.addToPath(manyPath, targetIdProp);
+ } else {
+ if (beanProp.isLob() && !beanProp.isFetchEager()) {
+ // AutoFetch will not include Lob's marked FetchLazy
+ // (which is the default for Lob's so typical).
+ } else {
+ pathProps.addToPath(path, beanProp.getName());
+ }
+ }
+ }
+ }
+
+ if ((modified || queryTuningAddVersion) && desc != null) {
+ BeanProperty[] versionProps = desc.propertiesVersion();
+ if (versionProps.length > 0) {
+ pathProps.addToPath(path, versionProps[0].getName());
+ }
+ }
+ }
+ }
+
+ public void publish(NodeUsageCollector profile) {
+
+ synchronized(monitor){
+
+ HashSet used = profile.getUsed();
+
+ profileCount++;
+ if (!used.isEmpty()){
+ profileUsedCount++;
+ aggregateUsed.addAll(used);
+ }
+ if (profile.isModified()){
+ modified = true;
+ }
+ }
+ }
+
+ public String toString() {
+ return "path["+path+"] profileCount["+profileCount+"] used["+profileUsedCount+"] props"+aggregateUsed;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/avaje/ebeaninternal/server/autofetch/StatisticsQuery.java b/src/main/java/com/avaje/ebeaninternal/server/autofetch/StatisticsQuery.java
new file mode 100644
index 000000000..d0b4bbd03
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/autofetch/StatisticsQuery.java
@@ -0,0 +1,42 @@
+package com.avaje.ebeaninternal.server.autofetch;
+
+import java.io.Serializable;
+
+import com.avaje.ebean.meta.MetaAutoFetchStatistic.QueryStats;
+
+/**
+ * Used to accumulate query execution statistics.
+ */
+public class StatisticsQuery implements Serializable {
+
+ private static final long serialVersionUID = -1133958958072778811L;
+
+ private final String path;
+
+ private int exeCount;
+
+ private int totalBeanLoaded;
+
+ private int totalMicros;
+
+ public StatisticsQuery(String path){
+ this.path = path;
+ }
+
+ public QueryStats createPublicMeta() {
+ return new QueryStats(path, exeCount, totalBeanLoaded, totalMicros);
+ }
+
+ public void add(int beansLoaded, int micros) {
+ exeCount++;
+ totalBeanLoaded += beansLoaded;
+ totalMicros += micros;
+ }
+
+ public String toString() {
+ long avgMicros = exeCount == 0 ? 0 : totalMicros / exeCount;
+
+ return "queryExe path["+path+"] count[" + exeCount + "] totalBeansLoaded[" + totalBeanLoaded + "] avgMicros["
+ + avgMicros + "] totalMicros[" + totalMicros + "]";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/avaje/ebeaninternal/server/autofetch/TunedQueryInfo.java b/src/main/java/com/avaje/ebeaninternal/server/autofetch/TunedQueryInfo.java
new file mode 100644
index 000000000..1a5dc9586
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/autofetch/TunedQueryInfo.java
@@ -0,0 +1,193 @@
+package com.avaje.ebeaninternal.server.autofetch;
+
+import java.io.Serializable;
+
+import com.avaje.ebean.bean.ObjectGraphOrigin;
+import com.avaje.ebean.meta.MetaAutoFetchTunedQueryInfo;
+import com.avaje.ebeaninternal.api.SpiQuery;
+import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetail;
+
+/**
+ * Holds tuned query information. Is immutable so this represents the tuning at
+ * a given point in time.
+ */
+public class TunedQueryInfo implements Serializable {
+
+ private static final long serialVersionUID = 7381493228797997282L;
+
+ private final ObjectGraphOrigin origin;
+
+ /**
+ * The tuned query details with joins and properties.
+ */
+ private OrmQueryDetail tunedDetail;
+
+ /**
+ * The number of times profiling has been collected for this query point.
+ */
+ private int profileCount;
+
+ private Long lastTuneTime = Long.valueOf(0);
+
+ private final String rateMonitor = new String();
+
+ /**
+ * The number of queries tuned by this object.
+ * Could use AtomicInteger perhaps.
+ */
+ private transient int tunedCount;
+
+ private transient int rateTotal;
+
+ private transient int rateHits;
+
+ private transient double lastRate;
+
+ public TunedQueryInfo(ObjectGraphOrigin queryPoint, OrmQueryDetail tunedDetail, int profileCount) {
+ this.origin = queryPoint;
+ this.tunedDetail = tunedDetail;
+ this.profileCount = profileCount;
+ }
+
+ /**
+ * Return true if this query should be profiled based on a percentage rate.
+ */
+ public boolean isPercentageProfile(double rate) {
+
+ synchronized (rateMonitor) {
+
+ if (lastRate != rate) {
+ // the rate has changed so resetting
+ lastRate = rate;
+ rateTotal = 0;
+ rateHits = 0;
+ }
+
+ rateTotal++;
+ if (rate > (double) rateHits / rateTotal) {
+ rateHits++;
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Create a copy of this tuned fetch data for public consumption.
+ */
+ public MetaAutoFetchTunedQueryInfo createPublicMeta() {
+ return new MetaAutoFetchTunedQueryInfo(origin, tunedDetail.toString(), profileCount, tunedCount, lastTuneTime);
+ }
+
+ /**
+ * Set the number of times profiling has been collected for this query
+ * point.
+ */
+ public void setProfileCount(int profileCount) {
+ // int assignment is atomic
+ this.profileCount = profileCount;
+ }
+
+ /**
+ * Set the tuned query detail.
+ */
+ public void setTunedDetail(OrmQueryDetail tunedDetail) {
+ // assignment is atomic
+ this.tunedDetail = tunedDetail;
+ this.lastTuneTime = Long.valueOf(System.currentTimeMillis());
+ }
+
+ /**
+ * Return true if the fetches are essentially the same.
+ */
+ public boolean isSame(OrmQueryDetail newQueryDetail) {
+ if (tunedDetail == null) {
+ return false;
+ }
+ return tunedDetail.isAutoFetchEqual(newQueryDetail);
+ }
+
+ /**
+ * Tune the query by replacing its OrmQueryDetail with a tuned one.
+ *
+ * @return true if the query was tuned, otherwise false.
+ */
+ public boolean autoFetchTune(SpiQuery> query) {
+ if (tunedDetail == null) {
+ return false;
+ }
+
+ boolean tuned = false;
+ //Note: tunedDetail is immutable by convention
+ if (query.isDetailEmpty()) {
+ tuned = true;
+ // tune by 'replacement'
+ query.setDetail(tunedDetail.copy());
+ } else {
+ // tune by 'addition'
+ tuned = query.tuneFetchProperties(tunedDetail);
+ }
+ if (tuned){
+ query.setAutoFetchTuned(true);
+ // a case for AtomicInteger but good enough for statistics
+ tunedCount++;
+ }
+ return tuned;
+ }
+
+ /**
+ * Return the time of the last tune.
+ */
+ public Long getLastTuneTime() {
+ return lastTuneTime;
+ }
+
+ /**
+ * Return the number of queries tuned by this object.
+ */
+ public int getTunedCount() {
+ return tunedCount;
+ }
+
+ /**
+ * Return the number of times profiling has been collected for this query
+ * point.
+ */
+ public int getProfileCount() {
+ return profileCount;
+ }
+
+ public OrmQueryDetail getTunedDetail() {
+ return tunedDetail;
+ }
+
+ public ObjectGraphOrigin getOrigin() {
+ return origin;
+ }
+
+ public String getLogOutput(OrmQueryDetail newQueryDetail) {
+
+ boolean changed = newQueryDetail != null;
+
+ StringBuilder sb = new StringBuilder(150);
+ sb.append( changed ? "\"Changed\",":"\"New\",");
+ sb.append("\"").append(origin.getBeanType()).append("\",");
+ sb.append("\"").append(origin.getKey()).append("\",");
+ if (changed){
+ sb.append("\"to: ").append(newQueryDetail.toString()).append("\",");
+ sb.append("\"from: ").append(tunedDetail.toString()).append("\",");
+ } else {
+ sb.append("\"to: ").append(tunedDetail.toString()).append("\",");
+ sb.append("\"\",");
+ }
+ sb.append("\"").append(origin.getFirstStackElement()).append("\"");
+
+ return sb.toString();
+ }
+
+ public String toString() {
+ return origin.getBeanType()+" "+origin.getKey()+" " + tunedDetail;
+ }
+
+}
diff --git a/src/main/java/com/avaje/ebeaninternal/server/autofetch/package.html b/src/main/java/com/avaje/ebeaninternal/server/autofetch/package.html
new file mode 100644
index 000000000..3b9b20417
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/autofetch/package.html
@@ -0,0 +1,8 @@
+
+
+AutoFetch Implementation
+
+
+AutoFetch Implementation
+
+
\ No newline at end of file
diff --git a/src/main/java/com/avaje/ebeaninternal/server/bean/BFAutoFetchStatisticFinder.java b/src/main/java/com/avaje/ebeaninternal/server/bean/BFAutoFetchStatisticFinder.java
new file mode 100644
index 000000000..6bf429f7d
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/bean/BFAutoFetchStatisticFinder.java
@@ -0,0 +1,79 @@
+package com.avaje.ebeaninternal.server.bean;
+
+import java.util.Iterator;
+
+import javax.persistence.PersistenceException;
+
+import com.avaje.ebean.bean.BeanCollection;
+import com.avaje.ebean.common.BeanList;
+import com.avaje.ebean.event.BeanFinder;
+import com.avaje.ebean.event.BeanQueryRequest;
+import com.avaje.ebean.meta.MetaAutoFetchStatistic;
+import com.avaje.ebeaninternal.api.SpiEbeanServer;
+import com.avaje.ebeaninternal.api.SpiQuery;
+import com.avaje.ebeaninternal.server.autofetch.AutoFetchManager;
+import com.avaje.ebeaninternal.server.autofetch.Statistics;
+
+/**
+ * Bean Finder for MetaAutoFetchStatistic.
+ *
+ * This gets the meta data from the AutoFetchManager and creates a copy of that
+ * data to give back to the caller in the form of MetaAutoFetchStatistic beans.
+ *
+ */
+public class BFAutoFetchStatisticFinder implements BeanFinder {
+
+
+ public MetaAutoFetchStatistic find(BeanQueryRequest request) {
+ SpiQuery query = (SpiQuery)request.getQuery();
+ try {
+ String queryPointKey = (String) query.getId();
+
+ SpiEbeanServer server = (SpiEbeanServer) request.getEbeanServer();
+ AutoFetchManager manager = server.getAutoFetchManager();
+
+ Statistics stats = manager.getStatistics(queryPointKey);
+ if (stats != null) {
+ return stats.createPublicMeta();
+ } else {
+ return null;
+ }
+
+ } catch (Exception e) {
+ throw new PersistenceException(e);
+ }
+ }
+
+ /**
+ * Only returns Lists at this stage.
+ */
+ public BeanCollection findMany(BeanQueryRequest request) {
+
+ SpiQuery.Type queryType = ((SpiQuery>)request.getQuery()).getType();
+ if (!queryType.equals(SpiQuery.Type.LIST)) {
+ throw new PersistenceException("Only findList() supported at this stage.");
+ }
+
+ SpiEbeanServer server = (SpiEbeanServer) request.getEbeanServer();
+ AutoFetchManager manager = server.getAutoFetchManager();
+
+ BeanList list = new BeanList();
+
+ Iterator it = manager.iterateStatistics();
+ while (it.hasNext()) {
+ Statistics stats = it.next();
+ // create a copy for public use
+ list.add(stats.createPublicMeta());
+ }
+
+ String orderBy = request.getQuery().order().toStringFormat();
+ if (orderBy == null){
+ orderBy = "beanType";
+ }
+ server.sort(list, orderBy);
+
+
+ return list;
+ }
+
+}
diff --git a/src/main/java/com/avaje/ebeaninternal/server/bean/BFAutoFetchTunedFetchFinder.java b/src/main/java/com/avaje/ebeaninternal/server/bean/BFAutoFetchTunedFetchFinder.java
new file mode 100644
index 000000000..598512b09
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/bean/BFAutoFetchTunedFetchFinder.java
@@ -0,0 +1,76 @@
+package com.avaje.ebeaninternal.server.bean;
+
+import java.util.Iterator;
+
+import javax.persistence.PersistenceException;
+
+import com.avaje.ebean.bean.BeanCollection;
+import com.avaje.ebean.common.BeanList;
+import com.avaje.ebean.event.BeanFinder;
+import com.avaje.ebean.event.BeanQueryRequest;
+import com.avaje.ebean.meta.MetaAutoFetchTunedQueryInfo;
+import com.avaje.ebeaninternal.api.SpiEbeanServer;
+import com.avaje.ebeaninternal.api.SpiQuery;
+import com.avaje.ebeaninternal.server.autofetch.AutoFetchManager;
+import com.avaje.ebeaninternal.server.autofetch.TunedQueryInfo;
+
+/**
+ * BeanFinder for MetaAutoFetchTunedFetch.
+ */
+public class BFAutoFetchTunedFetchFinder implements BeanFinder {
+
+
+ public MetaAutoFetchTunedQueryInfo find(BeanQueryRequest request) {
+
+ SpiQuery> query = (SpiQuery>)request.getQuery();
+ try {
+ String queryPointKey = (String)query.getId();
+
+ SpiEbeanServer server = (SpiEbeanServer) request.getEbeanServer();
+ AutoFetchManager manager = server.getAutoFetchManager();
+
+ TunedQueryInfo tunedFetch = manager.getTunedQueryInfo(queryPointKey);
+ if (tunedFetch != null){
+ return tunedFetch.createPublicMeta();
+ } else {
+ return null;
+ }
+
+ } catch (Exception e){
+ throw new PersistenceException(e);
+ }
+ }
+
+ /**
+ * Only returns Lists at this stage.
+ */
+ public BeanCollection findMany(BeanQueryRequest request) {
+
+ SpiQuery.Type queryType = ((SpiQuery>)request.getQuery()).getType();
+ if (!queryType.equals(SpiQuery.Type.LIST)){
+ throw new PersistenceException("Only findList() supported at this stage.");
+ }
+
+ SpiEbeanServer server = (SpiEbeanServer) request.getEbeanServer();
+ AutoFetchManager manager = server.getAutoFetchManager();
+
+ BeanList list = new BeanList();
+
+ Iterator it = manager.iterateTunedQueryInfo();
+ while (it.hasNext()) {
+ TunedQueryInfo tunedFetch = it.next();
+ // create a copy for public use
+ list.add(tunedFetch.createPublicMeta());
+ }
+
+ String orderBy = request.getQuery().order().toStringFormat();
+ if (orderBy == null){
+ orderBy = "beanType, origQueryPlanHash";
+ }
+ server.sort(list, orderBy);
+
+
+ return list;
+ }
+
+}
diff --git a/src/main/java/com/avaje/ebeaninternal/server/bean/BFQueryStatisticFinder.java b/src/main/java/com/avaje/ebeaninternal/server/bean/BFQueryStatisticFinder.java
new file mode 100644
index 000000000..b87df5eb7
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/bean/BFQueryStatisticFinder.java
@@ -0,0 +1,69 @@
+package com.avaje.ebeaninternal.server.bean;
+
+import java.util.Iterator;
+import java.util.List;
+
+import javax.persistence.PersistenceException;
+
+import com.avaje.ebean.bean.BeanCollection;
+import com.avaje.ebean.common.BeanList;
+import com.avaje.ebean.event.BeanFinder;
+import com.avaje.ebean.event.BeanQueryRequest;
+import com.avaje.ebean.meta.MetaQueryStatistic;
+import com.avaje.ebeaninternal.api.SpiEbeanServer;
+import com.avaje.ebeaninternal.api.SpiQuery;
+import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
+import com.avaje.ebeaninternal.server.query.CQueryPlan;
+
+/**
+ * BeanFinder for MetaQueryStatistic.
+ */
+public class BFQueryStatisticFinder implements BeanFinder {
+
+
+ public MetaQueryStatistic find(BeanQueryRequest request) {
+ throw new RuntimeException("Not Supported yet");
+ }
+
+ /**
+ * Only returns Lists at this stage.
+ */
+ public BeanCollection findMany(BeanQueryRequest request) {
+
+ SpiQuery.Type queryType = ((SpiQuery>)request.getQuery()).getType();
+ if (!queryType.equals(SpiQuery.Type.LIST)){
+ throw new PersistenceException("Only findList() supported at this stage.");
+ }
+
+ BeanList list = new BeanList();
+
+ SpiEbeanServer server = (SpiEbeanServer) request.getEbeanServer();
+ build(list, server);
+
+ String orderBy = request.getQuery().order().toStringFormat();
+ if (orderBy == null){
+ orderBy = "beanType, origQueryPlanHash, autofetchTuned";
+ }
+ server.sort(list, orderBy);
+
+ return list;
+ }
+
+ private void build(List list, SpiEbeanServer server) {
+
+ for (BeanDescriptor> desc : server.getBeanDescriptors()) {
+ desc.clearQueryStatistics();
+ build(list, desc);
+ }
+ }
+
+ private void build(List list, BeanDescriptor> desc) {
+
+ Iterator it = desc.queryPlans();
+ while (it.hasNext()) {
+ CQueryPlan queryPlan = (CQueryPlan) it.next();
+ list.add(queryPlan.createMetaQueryStatistic(desc.getFullName()));
+ }
+ }
+
+}
diff --git a/src/main/java/com/avaje/ebeaninternal/server/bean/package.html b/src/main/java/com/avaje/ebeaninternal/server/bean/package.html
new file mode 100644
index 000000000..f77109f52
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/bean/package.html
@@ -0,0 +1,8 @@
+
+
+BeanFinders, BeanControllers etc for "meta" beans
+
+
+BeanFinders, BeanControllers etc for "meta" beans
+
+
\ No newline at end of file
diff --git a/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanData.java b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanData.java
new file mode 100644
index 000000000..53ebf05a1
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanData.java
@@ -0,0 +1,50 @@
+package com.avaje.ebeaninternal.server.cache;
+
+import java.util.Set;
+
+public class CachedBeanData {
+
+ private final Object sharableBean;
+ private final Set loadedProperties;
+ private final Object[] data;
+ private final int naturalKeyUpdate;
+
+ public CachedBeanData(Object sharableBean, Set loadedProperties, Object[] data, int naturalKeyUpdate) {
+ this.sharableBean = sharableBean;
+ this.loadedProperties= loadedProperties;
+ this.data = data;
+ this.naturalKeyUpdate = naturalKeyUpdate;
+ }
+
+ public Object getSharableBean() {
+ return sharableBean;
+ }
+
+ public boolean isNaturalKeyUpdate() {
+ return naturalKeyUpdate > -1;
+ }
+
+ public Object getNaturalKey() {
+ return data[naturalKeyUpdate];
+ }
+
+ public boolean containsProperty(String propName) {
+ return loadedProperties == null || loadedProperties.contains(propName);
+ }
+
+ public Object getData(int i){
+ return data[i];
+ }
+
+ public Set getLoadedProperties() {
+ return loadedProperties;
+ }
+
+ public Object[] copyData() {
+ Object[] dest = new Object[data.length];
+ System.arraycopy(data, 0, dest, 0, data.length);
+ return dest;
+ }
+
+}
+
diff --git a/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataFromBean.java b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataFromBean.java
new file mode 100644
index 000000000..0390ffb78
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataFromBean.java
@@ -0,0 +1,103 @@
+package com.avaje.ebeaninternal.server.cache;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import com.avaje.ebean.bean.EntityBean;
+import com.avaje.ebean.bean.EntityBeanIntercept;
+import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
+import com.avaje.ebeaninternal.server.deploy.BeanProperty;
+
+public class CachedBeanDataFromBean {
+
+ private final BeanDescriptor> desc;
+ private final Object bean;
+ private final EntityBeanIntercept ebi;
+
+ private final Set loadedProps;
+ private final Set extractProps;
+
+ public static CachedBeanData extract(BeanDescriptor> desc, Object bean){
+ if (bean instanceof EntityBean){
+ return new CachedBeanDataFromBean(desc, bean, ((EntityBean)bean)._ebean_getIntercept()).extract();
+
+ } else {
+ return new CachedBeanDataFromBean(desc, bean, null).extract();
+ }
+ }
+
+ public static CachedBeanData extract(BeanDescriptor> desc, Object bean, EntityBeanIntercept ebi){
+ return new CachedBeanDataFromBean(desc, bean, ebi).extract();
+ }
+
+ private CachedBeanDataFromBean(BeanDescriptor> desc, Object bean, EntityBeanIntercept ebi) {
+ this.desc = desc;
+ this.bean = bean;
+ this.ebi = ebi;
+ if (ebi != null){
+ this.loadedProps = ebi.getLoadedProps();
+ this.extractProps = (loadedProps == null) ? null : new HashSet();
+ } else {
+ this.extractProps = new HashSet();
+ this.loadedProps = null;
+ }
+ }
+
+ private CachedBeanData extract(){
+
+ BeanProperty[] props = desc.propertiesNonMany();
+
+ Object[] data = new Object[props.length];
+
+ int naturalKeyUpdate = -1;
+ for (int i = 0; i < props.length; i++) {
+ BeanProperty prop = props[i];
+ if (includeNonManyProperty(prop.getName())){
+
+ data[i] = prop.getCacheDataValue(bean);
+ if (prop.isNaturalKey()) {
+ naturalKeyUpdate = i;
+ }
+ if (ebi != null){
+ if (extractProps != null){
+ extractProps.add(prop.getName());
+ }
+ } else if (data[i] != null){
+ if (extractProps != null){
+ extractProps.add(prop.getName());
+ }
+ }
+ }
+ }
+
+ Object sharableBean = null;
+ if (desc.isCacheSharableBeans() && ebi != null && loadedProps == null){
+ if (ebi.isReadOnly()){
+ sharableBean = bean;
+ } else {
+ // create a readOnly sharable instance by copying the data
+ sharableBean = desc.createBean(false);
+ BeanProperty[] propertiesId = desc.propertiesId();
+ for (int i = 0; i < propertiesId.length; i++) {
+ Object v = propertiesId[i].getValue(bean);
+ propertiesId[i].setValue(sharableBean, v);
+ }
+ BeanProperty[] propertiesNonTransient = desc.propertiesNonTransient();
+ for (int i = 0; i < propertiesNonTransient.length; i++) {
+ Object v = propertiesNonTransient[i].getValue(bean);
+ propertiesNonTransient[i].setValue(sharableBean, v);
+ }
+ EntityBeanIntercept ebi = ((EntityBean)sharableBean)._ebean_intercept();
+ ebi.setReadOnly(true);
+ ebi.setLoaded();
+ }
+ }
+
+ return new CachedBeanData(sharableBean, extractProps, data, naturalKeyUpdate);
+ }
+
+ private boolean includeNonManyProperty(String name) {
+ return loadedProps == null || loadedProps.contains(name);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataToBean.java b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataToBean.java
new file mode 100644
index 000000000..5199d8375
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataToBean.java
@@ -0,0 +1,117 @@
+package com.avaje.ebeaninternal.server.cache;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import com.avaje.ebean.bean.EntityBean;
+import com.avaje.ebean.bean.EntityBeanIntercept;
+import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
+import com.avaje.ebeaninternal.server.deploy.BeanProperty;
+import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocMany;
+
+public class CachedBeanDataToBean {
+
+ private final BeanDescriptor> desc;
+ private final Object bean;
+ private final EntityBeanIntercept ebi;
+ private final CachedBeanData cacheBeandata;
+ private final Set cacheLoadedProperties;
+ private final Set loadedProps;
+
+ private final Set excludeProps;
+ private final Object oldValuesBean;
+ private final boolean readOnly;
+
+ public static void load(BeanDescriptor> desc, Object bean, CachedBeanData cacheBeandata) {
+ if (bean instanceof EntityBean){
+ load(desc, bean, ((EntityBean)bean)._ebean_getIntercept(), cacheBeandata);
+ } else {
+ load(desc, bean, null, cacheBeandata);
+ }
+ }
+
+ public static void load(BeanDescriptor> desc, Object bean, EntityBeanIntercept ebi, CachedBeanData cacheBeandata) {
+ new CachedBeanDataToBean(desc, bean, ebi, cacheBeandata).load();
+ }
+
+ private CachedBeanDataToBean(BeanDescriptor> desc, Object bean, EntityBeanIntercept ebi, CachedBeanData cacheBeandata) {
+ this.desc = desc;
+ this.bean = bean;
+ this.ebi = ebi;
+ this.cacheBeandata = cacheBeandata;
+ this.cacheLoadedProperties = cacheBeandata.getLoadedProperties();
+ this.loadedProps = (cacheLoadedProperties == null) ? null : new HashSet();
+
+ if (ebi != null){
+ this.excludeProps = ebi.getLoadedProps();
+ this.oldValuesBean = ebi.getOldValues();
+ this.readOnly = ebi.isReadOnly();
+ } else {
+ this.excludeProps = null;
+ this.oldValuesBean = null;
+ this.readOnly = false;
+ }
+ }
+
+ private boolean load(){
+
+ BeanProperty[] propertiesNonTransient = desc.propertiesNonMany();
+ for (int i = 0; i < propertiesNonTransient.length; i++) {
+ BeanProperty prop = propertiesNonTransient[i];
+ if (includeNonManyProperty(prop.getName())){
+ Object data = cacheBeandata.getData(i);
+ prop.setCacheDataValue(bean, data, oldValuesBean, readOnly);
+ }
+ }
+ BeanPropertyAssocMany>[] manys = desc.propertiesMany();
+ for (int i = 0; i < manys.length; i++) {
+ BeanPropertyAssocMany> prop = manys[i];
+ if (includeManyProperty(prop.getName())){
+ // set a lazy loading proxy
+ prop.createReference(bean);
+ }
+ }
+
+ if (ebi != null){
+ if (loadedProps == null){
+ ebi.setLoadedProps(null);
+ } else {
+ HashSet mergeProps = new HashSet();
+ if (excludeProps != null) {
+ mergeProps.addAll(excludeProps);
+ }
+ mergeProps.addAll(loadedProps);
+ ebi.setLoadedProps(mergeProps);
+ }
+ ebi.setLoadedLazy();
+ }
+
+ return true;
+ }
+
+ private boolean includeManyProperty(String name) {
+ if (excludeProps != null && excludeProps.contains(name)){
+ // ignore this property (partial bean lazy loading)
+ return false;
+ }
+ if (loadedProps != null){
+ loadedProps.add(name);
+ }
+ return true;
+ }
+
+ private boolean includeNonManyProperty(String name) {
+ if (excludeProps != null && excludeProps.contains(name)){
+ // ignore this property (partial bean lazy loading)
+ return false;
+ }
+ if (cacheLoadedProperties != null && !cacheLoadedProperties.contains(name)){
+ return false;
+ }
+ if (loadedProps != null){
+ loadedProps.add(name);
+ }
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataUpdate.java b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataUpdate.java
new file mode 100644
index 000000000..93c883213
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataUpdate.java
@@ -0,0 +1,49 @@
+package com.avaje.ebeaninternal.server.cache;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import com.avaje.ebeaninternal.server.core.PersistRequestBean;
+import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
+import com.avaje.ebeaninternal.server.deploy.BeanProperty;
+
+public class CachedBeanDataUpdate {
+
+ public static CachedBeanData update(BeanDescriptor> desc, CachedBeanData data, PersistRequestBean> updateRequest){
+
+
+ Set loadedProperties = data.getLoadedProperties();
+ Object[] copyOfData = data.copyData();
+
+ Object updateBean = updateRequest.getBean();
+ Set updatedProperties = updateRequest.getUpdatedProperties();
+
+ int naturalKeyUpdate = -1;
+ boolean mergeProperties = false;
+ BeanProperty[] props = desc.propertiesNonMany();
+ for (int i = 0; i < props.length; i++) {
+ if (updatedProperties.contains(props[i].getName())){
+ if (props[i].isNaturalKey()){
+ naturalKeyUpdate = i;
+ }
+ copyOfData[i] = props[i].getCacheDataValue(updateBean);
+ if (loadedProperties != null && !mergeProperties && !loadedProperties.contains(props[i].getName())){
+ mergeProperties = true;
+ }
+ }
+ }
+
+ if (mergeProperties){
+ HashSet mergeProps = new HashSet();
+ mergeProps.addAll(loadedProperties);
+ mergeProps.addAll(updatedProperties);
+ loadedProperties = mergeProps;
+ }
+
+ return new CachedBeanData(null, loadedProperties, copyOfData, naturalKeyUpdate);
+
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/avaje/ebeaninternal/server/cache/CachedManyIds.java b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedManyIds.java
new file mode 100644
index 000000000..3f123efa7
--- /dev/null
+++ b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedManyIds.java
@@ -0,0 +1,17 @@
+package com.avaje.ebeaninternal.server.cache;
+
+import java.util.List;
+
+public class CachedManyIds {
+
+ private final List