diff --git a/src/main/java/com/avaje/ebean/AdminAutofetch.java b/src/main/java/com/avaje/ebean/AdminAutofetch.java new file mode 100644 index 000000000..c9c7046ee --- /dev/null +++ b/src/main/java/com/avaje/ebean/AdminAutofetch.java @@ -0,0 +1,137 @@ +package com.avaje.ebean; + +/** + * Administrative control of Autofetch during runtime. + */ +public interface AdminAutofetch { + + /** + * Return true if profiling is enabled. + */ + public boolean isProfiling(); + + /** + * Set to true to enable profiling. + */ + public void setProfiling(boolean enable); + + /** + * Return true if autoFetch automatic query tuning is enabled. + */ + public boolean isQueryTuning(); + + /** + * Set to true to enable autoFetch automatic query tuning. + */ + public void setQueryTuning(boolean enable); + + /** + * Returns the rate which profiling is collected. This is an int between 0 and + * 100. + */ + public double getProfilingRate(); + + /** + * Set the rate at which profiling is collected after the base. + * + * @param rate + * a int between 0 and 100. + */ + public void setProfilingRate(double rate); + + /** + * Return the number of queries profiled after which profiling is collected at + * a percentage rate. + */ + public int getProfilingBase(); + + /** + * Set a base number of queries to profile per query point. + *

+ * After this amount of profiling has been obtained profiling is collected at + * the Profiling Percentage rate. + *

+ */ + public void setProfilingBase(int profilingBase); + + /** + * 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(); + + /** + * This will take the current profiling information and update the "tuned + * query detail". + *

+ * This is done periodically and can also be manually invoked. + *

+ * + * @return a summary of the updates that occurred + */ + public String updateTunedQueryInfo(); + + /** + * Clear all the tuned query info. + *

+ * Should only need do this for testing and playing around. + *

+ * + * @return the amount of tuned query information cleared. + */ + public int clearTunedQueryInfo(); + + /** + * Clear all the profiling information. + *

+ * This means the profiling information will need to be re-gathered. + *

+ *

+ * Should only need do this for testing and playing around. + *

+ * + * @return the amount of profiled information cleared. + */ + public int clearProfilingInfo(); + + /** + * Clear the query execution statistics. + */ + public void clearQueryStatistics(); + + /** + * 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(); + +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/BackgroundExecutor.java b/src/main/java/com/avaje/ebean/BackgroundExecutor.java new file mode 100644 index 000000000..f3b86b79b --- /dev/null +++ b/src/main/java/com/avaje/ebean/BackgroundExecutor.java @@ -0,0 +1,40 @@ +package com.avaje.ebean; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Background thread pool service for executing of tasks asynchronously. + *

+ * This service is used internally by Ebean for executing background tasks such + * as the {@link Query#findFutureList()} and also for executing background tasks + * periodically. + *

+ *

+ * This service has been made available so you can use it for your application + * code if you want. It can be useful for some server caching implementations + * (background population and trimming of the cache etc). + *

+ * + * @author rbygrave + */ +public interface BackgroundExecutor { + + /** + * Execute a task in the background. + */ + public void execute(Runnable r); + + /** + * Execute a task periodically with a fixed delay between each execution. + *

+ * For example, execute a runnable every minute. + *

+ *

+ * The delay is the time between executions no matter how long the task took. + * That is, this method has the same behaviour characteristics as + * {@link ScheduledExecutorService#scheduleWithFixedDelay(Runnable, long, long, TimeUnit)} + *

+ */ + public void executePeriodically(Runnable r, long delay, TimeUnit unit); +} diff --git a/src/main/java/com/avaje/ebean/BeanState.java b/src/main/java/com/avaje/ebean/BeanState.java new file mode 100644 index 000000000..fc4e3c371 --- /dev/null +++ b/src/main/java/com/avaje/ebean/BeanState.java @@ -0,0 +1,94 @@ +package com.avaje.ebean; + +import java.beans.PropertyChangeListener; +import java.util.Set; + +/** + * Provides access to the internal state of an entity bean. + */ +public interface BeanState { + + /** + * Return true if this is a lazy loading reference bean. + *

+ * If so the this bean only holds the Id property and will invoke lazy loading + * if any other property is get or set. + *

+ */ + public boolean isReference(); + + /** + * Return true if the bean is new (and not yet saved). + */ + public boolean isNew(); + + /** + * Return true if the bean is new or dirty (and probably needs to be saved). + */ + public boolean isNewOrDirty(); + + /** + * Return true if the bean has been changed but not yet saved. + */ + public boolean isDirty(); + + /** + * For partially populated beans returns the properties that are loaded on the + * bean. + *

+ * Accessing another property will cause lazy loading to occur. + *

+ */ + public Set getLoadedProps(); + + /** + * Return the set of changed properties. + */ + public Set getChangedProps(); + + /** + * Return true if the bean is readOnly. + *

+ * If a setter is called on a readOnly bean it will throw an exception. + *

+ */ + public boolean isReadOnly(); + + /** + * Set the readOnly status for the bean. + */ + public void setReadOnly(boolean readOnly); + + /** + * Add a propertyChangeListener. + */ + public void addPropertyChangeListener(PropertyChangeListener listener); + + /** + * Remove a propertyChangeListener. + */ + public void removePropertyChangeListener(PropertyChangeListener listener); + + /** + * Advanced - Used to programmatically build a reference object. + *

+ * You can create a new EntityBean ( + * {@link EbeanServer#createEntityBean(Class)}, set its Id property and then + * call this setReference() method. + *

+ */ + public void setReference(); + + /** + * Advanced - Used to programmatically build a partially or fully loaded + * entity bean. First create an entity bean via + * {@link EbeanServer#createEntityBean(Class)}, then populate its properties + * and then call this method specifying which properties where loaded or null + * for a fully loaded entity bean. + * + * @param loadedProperties + * the properties that where loaded or null for a fully loaded entity + * bean. + */ + public void setLoaded(Set loadedProperties); +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/CallableSql.java b/src/main/java/com/avaje/ebean/CallableSql.java new file mode 100644 index 000000000..d6e13f635 --- /dev/null +++ b/src/main/java/com/avaje/ebean/CallableSql.java @@ -0,0 +1,194 @@ +package com.avaje.ebean; + +import java.sql.CallableStatement; +import java.sql.SQLException; + +/** + * For making calls to stored procedures. Refer to the Ebean execute() method. + *

+ * Note that UpdateSql is designed for general DML sql and CallableSql is + * designed for use with stored procedures. Also note that when using this in + * batch mode the out parameters are not read. + *

+ *

+ * Example 1: + *

+ * + *
+ * String sql = "{call sp_order_mod(?,?)}";
+ * 
+ * CallableSql cs = Ebean.createCallableSql(sql);
+ * cs.setParameter(1, "turbo");
+ * cs.registerOut(2, Types.INTEGER);
+ * 
+ * Ebean.execute(cs);
+ * 
+ * // read the out parameter
+ * Integer returnValue = (Integer) cs.getObject(2);
+ * 
+ * + *

+ * Example 2:
+ * Includes batch mode, table modification information and label. Note that the + * label is really only to help people reading the transaction logs to identify + * the procedure called etc. + *

+ * + *
+ * String sql = "{call sp_insert_order(?,?)}";
+ * 
+ * CallableSql cs = Ebean.createCallableSql(sql);
+ * 
+ * // Inform Ebean this stored procedure inserts into the
+ * // oe_order table and inserts + updates the oe_order_detail table.
+ * // this is used to invalidate objects in the cache
+ * cs.addModification("oe_order", true, false, false);
+ * cs.addModification("oe_order_detail", true, true, false);
+ * 
+ * Transaction t = Ebean.startTransaction();
+ * 
+ * // execute using JDBC batching 10 statements at a time
+ * t.setBatchMode(true);
+ * t.setBatchSize(10);
+ * try {
+ *   cs.setParameter(1, "Was");
+ *   cs.setParameter(2, "Banana");
+ *   Ebean.execute(cs);
+ * 
+ *   cs.setParameter(1, "Here");
+ *   cs.setParameter(2, "Kumera");
+ *   Ebean.execute(cs);
+ * 
+ *   cs.setParameter(1, "More");
+ *   cs.setParameter(2, "Apple");
+ *   Ebean.execute(cs);
+ * 
+ *   // Ebean.externalModification("oe_order",true,false,false);
+ *   // Ebean.externalModification("oe_order_detail",true,true,false);
+ *   Ebean.commitTransaction();
+ * 
+ * } finally {
+ *   Ebean.endTransaction();
+ * }
+ * 
+ * + * @see com.avaje.ebean.SqlUpdate + * @see com.avaje.ebean.Ebean#execute(CallableSql) + */ +public interface CallableSql { + + /** + * Return the label that is put into the transaction log. + */ + public String getLabel(); + + /** + * Set the label that is put in the transaction log. + */ + public CallableSql setLabel(String label); + + /** + * Return the statement execution timeout. + */ + public int getTimeout(); + + /** + * Return the callable sql. + */ + public String getSql(); + + /** + * Set the statement execution timeout. Zero implies unlimited time. + *

+ * This is set to the underlying CallableStatement. + *

+ */ + public CallableSql setTimeout(int secs); + + /** + * Set the callable sql. + */ + public CallableSql setSql(String sql); + + /** + * Bind a parameter that is bound as a IN parameter. + *

+ * position starts at value 1 (not 0) to be consistent with CallableStatement. + *

+ *

+ * This is designed so that you do not need to set params in index order. You + * can set/register param 2 before param 1 etc. + *

+ * + * @param position + * the index position of the parameter. + * @param value + * the value of the parameter. + */ + public CallableSql bind(int position, Object value); + + /** + * Bind a positioned parameter (same as bind method). + * + * @param position + * the index position of the parameter. + * @param value + * the value of the parameter. + */ + public CallableSql setParameter(int position, Object value); + + /** + * Register an OUT parameter. + *

+ * Note that position starts at value 1 (not 0) to be consistent with + * CallableStatement. + *

+ *

+ * This is designed so that you do not need to register params in index order. + * You can set/register param 2 before param 1 etc. + *

+ * + * @param position + * the index position of the parameter (starts with 1). + * @param type + * the jdbc type of the OUT parameter that will be read. + */ + public CallableSql registerOut(int position, int type); + + /** + * Return an OUT parameter value. + *

+ * position starts at value 1 (not 0) to be consistent with CallableStatement. + *

+ *

+ * This can only be called after the CallableSql has been executed. When run + * in batch mode you effectively can't use this method. + *

+ */ + public Object getObject(int position); + + /** + * + * You can extend this object and override this method for more advanced + * stored procedure calls. This would be the case when ResultSets are returned + * etc. + */ + public boolean executeOverride(CallableStatement cstmt) throws SQLException; + + /** + * Add table modification information to the TransactionEvent. + *

+ * This would be similar to using the + * Ebean.externalModification() method. It may be easier and make + * more sense to set it here with the CallableSql. + *

+ *

+ * For UpdateSql the table modification information is derived by parsing the + * sql to determine the table name and whether it was an insert, update or + * delete. + *

+ */ + public CallableSql addModification(String tableName, boolean inserts, boolean updates, + boolean deletes); + +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/DRawSqlColumnsParser.java b/src/main/java/com/avaje/ebean/DRawSqlColumnsParser.java new file mode 100644 index 000000000..058990467 --- /dev/null +++ b/src/main/java/com/avaje/ebean/DRawSqlColumnsParser.java @@ -0,0 +1,93 @@ +package com.avaje.ebean; + +import java.util.ArrayList; +import java.util.Arrays; + +import javax.persistence.PersistenceException; + +import com.avaje.ebean.RawSql.ColumnMapping; + +/** + * Parses columnMapping (select clause) mapping columns to bean properties. + */ +final class DRawSqlColumnsParser { + + private final int end; + + private final String sqlSelect; + + private int pos; + + private int indexPos; + + public static ColumnMapping parse(String sqlSelect) { + return new DRawSqlColumnsParser(sqlSelect).parse(); + } + + private DRawSqlColumnsParser(String sqlSelect) { + this.sqlSelect = sqlSelect; + this.end = sqlSelect.length(); + } + + private ColumnMapping parse() { + + ArrayList columns = new ArrayList(); + while (pos <= end) { + ColumnMapping.Column c = nextColumnInfo(); + columns.add(c); + } + + return new ColumnMapping(columns); + } + + private ColumnMapping.Column nextColumnInfo() { + int start = pos; + nextComma(); + String colInfo = sqlSelect.substring(start, pos++); + colInfo = colInfo.trim(); + + String[] split = colInfo.split(" "); + if (split.length > 1) { + ArrayList tmp = new ArrayList(split.length); + for (int i = 0; i < split.length; i++) { + if (split[i].trim().length() > 0) { + tmp.add(split[i].trim()); + } + } + split = tmp.toArray(new String[tmp.size()]); + } + + if (split.length == 1) { + // default to column the same name as the property + return new ColumnMapping.Column(indexPos++, split[0], null); + } + if (split.length == 2) { + return new ColumnMapping.Column(indexPos++, split[0], split[1]); + } + if (split.length == 3) { + if (!split[1].equalsIgnoreCase("as")) { + String msg = "Expecting AS keyword parsing column " + colInfo; + throw new PersistenceException(msg); + } + return new ColumnMapping.Column(indexPos++, split[0], split[2]); + } + + String msg = "Expecting Max 3 words parsing column " + colInfo + ". Got " + + Arrays.toString(split); + throw new PersistenceException(msg); + } + + private int nextComma() { + boolean inQuote = false; + while (pos < end) { + char c = sqlSelect.charAt(pos); + if (c == '\'') { + inQuote = !inQuote; + } else if (!inQuote && c == ',') { + return pos; + } + pos++; + } + return pos; + } +} diff --git a/src/main/java/com/avaje/ebean/DRawSqlParser.java b/src/main/java/com/avaje/ebean/DRawSqlParser.java new file mode 100644 index 000000000..eaa196146 --- /dev/null +++ b/src/main/java/com/avaje/ebean/DRawSqlParser.java @@ -0,0 +1,298 @@ +package com.avaje.ebean; + +import com.avaje.ebean.RawSql.Sql; + +/** + * Parses sql-select queries to try and determine the location where WHERE and + * HAVING clauses can be added dynamically to the sql. + */ +class DRawSqlParser { + + public static final String $_AND_HAVING = "${andHaving}"; + + public static final String $_HAVING = "${having}"; + + public static final String $_AND_WHERE = "${andWhere}"; + + public static final String $_WHERE = "${where}"; + + private static final String ORDER_BY = "order by"; + + private final SimpleTextParser textParser; + + private String sql; + + private int placeHolderWhere; + private int placeHolderAndWhere; + private int placeHolderHaving; + private int placeHolderAndHaving; + private boolean hasPlaceHolders; + + private int selectPos = -1; + private int distinctPos = -1; + private int fromPos = -1; + private int wherePos = -1; + private int groupByPos = -1; + private int havingPos = -1; + private int orderByPos = -1; + + private boolean whereExprAnd; + private int whereExprPos = -1; + private boolean havingExprAnd; + private int havingExprPos = -1; + + public static Sql parse(String sql) { + return new DRawSqlParser(sql).parse(); + } + + private DRawSqlParser(String sqlString) { + sqlString = sqlString.trim(); + this.sql = sqlString; + this.hasPlaceHolders = findAndRemovePlaceHolders(); + this.textParser = new SimpleTextParser(sqlString); + } + + private Sql parse() { + + if (!hasPlaceHolders()) { + // parse the sql for the keywords... + // select, from, where, having, group by, order by + parseSqlFindKeywords(true); + } + + whereExprPos = findWhereExprPosition(); + havingExprPos = findHavingExprPosition(); + + String preFrom = removeWhitespace(findPreFromSql()); + String preWhere = removeWhitespace(findPreWhereSql()); + String preHaving = removeWhitespace(findPreHavingSql()); + String orderBySql = findOrderBySql(); + + preFrom = trimSelectKeyword(preFrom); + + return new Sql(sql.hashCode(), preFrom, preWhere, whereExprAnd, preHaving, havingExprAnd, + orderBySql, (distinctPos > -1)); + } + + /** + * Find and remove the known place holders such as ${where}. + */ + private boolean findAndRemovePlaceHolders() { + placeHolderWhere = removePlaceHolder($_WHERE); + placeHolderAndWhere = removePlaceHolder($_AND_WHERE); + placeHolderHaving = removePlaceHolder($_HAVING); + placeHolderAndHaving = removePlaceHolder($_AND_HAVING); + return hasPlaceHolders(); + } + + private int removePlaceHolder(String placeHolder) { + int pos = sql.indexOf(placeHolder); + if (pos > -1) { + int after = pos + placeHolder.length() + 1; + if (after > sql.length()) { + sql = sql.substring(0, pos); + } else { + sql = sql.substring(0, pos) + sql.substring(after); + } + } + return pos; + } + + private boolean hasPlaceHolders() { + if (placeHolderWhere > -1) { + return true; + } + if (placeHolderAndWhere > -1) { + return true; + } + if (placeHolderHaving > -1) { + return true; + } + if (placeHolderAndHaving > -1) { + return true; + } + return false; + } + + /** + * Trim off the select keyword (to support row_number() limit function). + */ + private String trimSelectKeyword(String preWhereExprSql) { + + if (selectPos < 0) { + throw new IllegalStateException("select keyword not found?"); + } + // trim of select keyword + preWhereExprSql = preWhereExprSql.trim(); + String select = preWhereExprSql.substring(0, 7); + if (!select.equalsIgnoreCase("select ")) { + throw new RuntimeException("Expecting [" + preWhereExprSql + "] to start with \"select\""); + } + preWhereExprSql = preWhereExprSql.substring(7).trim(); + if (distinctPos > -1) { + // trim of distinct keyword + String distinct = preWhereExprSql.substring(0, 9); + if (!distinct.equalsIgnoreCase("distinct ")) { + throw new RuntimeException("Expecting [" + preWhereExprSql + + "] to start with \"select distinct\""); + } + preWhereExprSql = preWhereExprSql.substring(9); + } + + return preWhereExprSql; + } + + private String findOrderBySql() { + if (orderByPos > -1) { + int pos = orderByPos + ORDER_BY.length(); + return sql.substring(pos).trim(); + } + return null; + } + + private String findPreHavingSql() { + if (havingExprPos > whereExprPos) { + // an order by clause follows... + return sql.substring(whereExprPos, havingExprPos - 1); + } + if (whereExprPos > -1) { + if (orderByPos == -1) { + return sql.substring(whereExprPos); + + } else if (whereExprPos == orderByPos) { + return ""; + + } else { + return sql.substring(whereExprPos, orderByPos - 1); + } + } + return null; + } + + private String findPreFromSql() { + return sql.substring(0, fromPos - 1); + } + + private String findPreWhereSql() { + if (whereExprPos > -1) { + return sql.substring(fromPos, whereExprPos - 1); + } else { + return sql.substring(fromPos); + } + } + + private void parseSqlFindKeywords(boolean allKeywords) { + + selectPos = textParser.findWordLower("select"); + if (selectPos == -1) { + String msg = "Error parsing sql, can not find SELECT keyword in:"; + throw new RuntimeException(msg + sql); + } + + String possibleDistinct = textParser.nextWord(); + if ("distinct".equals(possibleDistinct)) { + distinctPos = textParser.getPos() - 8; + } + + fromPos = textParser.findWordLower("from"); + if (fromPos == -1) { + String msg = "Error parsing sql, can not find FROM keyword in:"; + throw new RuntimeException(msg + sql); + } + + if (!allKeywords) { + return; + } + + wherePos = textParser.findWordLower("where"); + if (wherePos == -1) { + groupByPos = textParser.findWordLower("group", fromPos + 5); + } else { + groupByPos = textParser.findWordLower("group"); + } + if (groupByPos > -1) { + havingPos = textParser.findWordLower("having"); + } + + int startOrderBy = havingPos; + if (startOrderBy == -1) { + startOrderBy = groupByPos; + } + if (startOrderBy == -1) { + startOrderBy = wherePos; + } + if (startOrderBy == -1) { + startOrderBy = fromPos; + } + + orderByPos = textParser.findWordLower("order", startOrderBy); + } + + private int findWhereExprPosition() { + if (hasPlaceHolders) { + if (placeHolderWhere > -1) { + return placeHolderWhere; + } else { + whereExprAnd = true; + return placeHolderAndWhere; + } + } + whereExprAnd = wherePos > 0; + if (groupByPos > 0) { + return groupByPos; + } + if (havingPos > 0) { + return havingPos; + } + if (orderByPos > 0) { + return orderByPos; + } + return -1; + } + + private int findHavingExprPosition() { + if (hasPlaceHolders) { + if (placeHolderHaving > -1) { + return placeHolderHaving; + } else { + havingExprAnd = true; + return placeHolderAndHaving; + } + } + havingExprAnd = havingPos > 0; + if (orderByPos > 0) { + return orderByPos; + } + return -1; + } + + private String removeWhitespace(String sql) { + if (sql == null) { + return ""; + } + + boolean removeWhitespace = false; + + int length = sql.length(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + char c = sql.charAt(i); + if (removeWhitespace) { + if (!Character.isWhitespace(c)) { + sb.append(c); + removeWhitespace = false; + } + } else { + if (c == '\r' || c == '\n') { + sb.append('\n'); + removeWhitespace = true; + } else { + sb.append(c); + } + } + } + + String s = sb.toString(); + return s.trim(); + } +} diff --git a/src/main/java/com/avaje/ebean/Ebean.java b/src/main/java/com/avaje/ebean/Ebean.java new file mode 100644 index 000000000..b8f6a86f5 --- /dev/null +++ b/src/main/java/com/avaje/ebean/Ebean.java @@ -0,0 +1,1364 @@ +package com.avaje.ebean; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.persistence.OptimisticLockException; +import javax.persistence.PersistenceException; + +import com.avaje.ebean.annotation.CacheStrategy; +import com.avaje.ebean.cache.ServerCacheManager; +import com.avaje.ebean.config.GlobalProperties; +import com.avaje.ebean.config.ServerConfig; +import com.avaje.ebean.text.csv.CsvReader; +import com.avaje.ebean.text.json.JsonContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This Ebean object is effectively a singleton that holds a map of registered + * {@link EbeanServer}s. It additionally provides a convenient way to use the + * 'default/primary' EbeanServer. + *

+ * If you are using a Dependency Injection framework such as + * Spring or Guice you will probably + * NOT use this Ebean singleton object. Instead you will + * configure and construct EbeanServer instances using {@link ServerConfig} and + * {@link EbeanServerFactory} and inject those EbeanServer instances into your + * data access objects. + *

+ *

+ * In documentation "Ebean singleton" refers to this object. + *

+ *
    + *
  • There is one EbeanServer per Database (javax.sql.DataSource).
  • + *
  • EbeanServers can be 'registered' with the Ebean singleton (put into its + * map). Registered EbeanServer's can later be retrieved via + * {@link #getServer(String)}.
  • + *
  • One EbeanServer can be referred to as the 'default' EbeanServer. For + * convenience, the Ebean singleton (this object) provides methods such as + * {@link #find(Class)} that proxy through to the 'default' EbeanServer. This + * can be useful for applications that use a single database.
  • + *
+ * + *

+ * For developer convenience Ebean has static methods that proxy through to the + * methods on the 'default' EbeanServer. These methods are provided for + * developers who are mostly using a single database. Many developers will be + * able to use the methods on Ebean rather than get a EbeanServer. + *

+ *

+ * EbeanServers can be created and used without ever needing or using the Ebean + * singleton. Refer to {@link ServerConfig#setRegister(boolean)}. + *

+ *

+ * You can either programmatically create/register EbeanServers via + * {@link EbeanServerFactory} or they can automatically be created and + * registered when you first use the Ebean singleton. When EbeanServers are + * created automatically they are configured using information in the + * ebean.properties file. + *

+ * + *
+ * // fetch shipped orders (and also their customer)
+ * List<Order> list = Ebean.find(Order.class)
+ * 	.fetch("customer")
+ * 	.where() 
+ * 	.eq("status.code", Order.Status.SHIPPED) 
+ * 	.findList();
+ * 
+ * // read/use the order list ... 
+ * for (Order order : list) { 
+ * 	Customer customer = order.getCustomer(); 
+ * 	... 
+ * }
+ * 
+ * + *
+ * // fetch order 10, modify and save 
+ * Order order = Ebean.find(Order.class, 10);
+ * 
+ * OrderStatus shipped = Ebean.getReference(OrderStatus.class,"SHIPPED"); 
+ * order.setStatus(shipped);
+ * order.setShippedDate(shippedDate); 
+ * ...
+ * 
+ * // implicitly creates a transaction and commits 
+ * Ebean.save(order);
+ * 
+ * + *

+ * When you have multiple databases and need access to a specific one the + * {@link #getServer(String)} method provides access to the EbeanServer for that + * specific database. + *

+ * + *
+ * // Get access to the Human Resources EbeanServer/Database
+ * EbeanServer hrDb = Ebean.getServer("hr");
+ * 
+ * 
+ * // fetch contact 3 from the HR database 
+ * Contact contact = hrDb.find(Contact.class, 3);
+ * 
+ * contact.setName("I'm going to change"); 
+ * ...
+ * 
+ * // save the contact back to the HR database 
+ * hrDb.save(contact);
+ * 
+ */ +public final class Ebean { + private static final Logger logger = LoggerFactory.getLogger(Ebean.class); + + /** + * Manages creation and cache of EbeanServers. + */ + private static final Ebean.ServerManager serverMgr = new Ebean.ServerManager(); + + /** + * Helper class for managing fast and safe access and creation of + * EbeanServers. + */ + private static final class ServerManager { + + /** + * Cache for fast concurrent read access. + */ + private final ConcurrentHashMap concMap = new ConcurrentHashMap(); + + /** + * Cache for synchronized read, creation and put. Protected by the monitor + * object. + */ + private final HashMap syncMap = new HashMap(); + + private final Object monitor = new Object(); + + /** + * The 'default/primary' EbeanServer. + */ + private EbeanServer primaryServer; + + private ServerManager() { + + // skipDefaultServer is set by EbeanServerFactory + // ... when it is creating the primaryServer + if (GlobalProperties.isSkipPrimaryServer()) { + // primary server being created by EbeanServerFactory + // ... so we should not try and create it here + logger.debug("GlobalProperties.isSkipPrimaryServer()"); + + } else { + // look to see if there is a default server defined + String primaryName = getPrimaryServerName(); + logger.debug("primaryName:" + primaryName); + if (primaryName != null && primaryName.trim().length() > 0) { + primaryServer = getWithCreate(primaryName.trim()); + } + } + } + + private String getPrimaryServerName() { + + String serverName = GlobalProperties.get("ebean.default.datasource", null); + return GlobalProperties.get("datasource.default", serverName); + } + + private EbeanServer getPrimaryServer() { + if (primaryServer == null) { + String msg = "The default EbeanServer has not been defined?"; + msg += " This is normally set via the ebean.datasource.default property."; + msg += " Otherwise it should be registered programatically via registerServer()"; + throw new PersistenceException(msg); + } + return primaryServer; + } + + private EbeanServer get(String name) { + if (name == null || name.length() == 0) { + return primaryServer; + } + // non-synchronized read + EbeanServer server = concMap.get(name); + if (server != null) { + return server; + } + // synchronized read, create and put + return getWithCreate(name); + } + + /** + * Synchronized read, create and put of EbeanServers. + */ + private EbeanServer getWithCreate(String name) { + + synchronized (monitor) { + + EbeanServer server = syncMap.get(name); + if (server == null) { + // register when creating server this way + server = EbeanServerFactory.create(name); + register(server, false); + } + return server; + } + } + + /** + * Register a server so we can get it by its name. + */ + private void register(EbeanServer server, boolean isPrimaryServer) { + synchronized (monitor) { + concMap.put(server.getName(), server); + EbeanServer existingServer = syncMap.put(server.getName(), server); + if (existingServer != null) { + String msg = "Existing EbeanServer [" + server.getName() + "] is being replaced?"; + logger.warn(msg); + } + + if (isPrimaryServer) { + primaryServer = server; + } + } + } + + } + + private Ebean() { + } + + /** + * Get the EbeanServer for a given DataSource. If name is null this will + * return the 'default' EbeanServer. + *

+ * This is provided to access EbeanServer for databases other than the + * 'default' database. EbeanServer also provides more control over + * transactions and the ability to use transactions created externally to + * Ebean. + *

+ * + *
+   * // use the "hr" database
+   * EbeanServer hrDatabase = Ebean.getServer("hr");
+   * 
+   * Person person = hrDatabase.find(Person.class, 10);
+   * 
+ * + * @param name + * the name of the server, use null for the 'default server' + */ + public static EbeanServer getServer(String name) { + return serverMgr.get(name); + } + + /** + * Return the ExpressionFactory from the default server. + *

+ * The ExpressionFactory is used internally by the query and ExpressionList to + * build the WHERE and HAVING clauses. Alternatively you can use the + * ExpressionFactory directly to create expressions to add to the query where + * clause. + *

+ *

+ * Alternatively you can use the {@link Expr} as a shortcut to the + * ExpressionFactory of the 'Default' EbeanServer. + *

+ *

+ * You generally need to the an ExpressionFactory (or {@link Expr}) to build + * an expression that uses OR like Expression e = Expr.or(..., ...); + *

+ */ + public static ExpressionFactory getExpressionFactory() { + return serverMgr.getPrimaryServer().getExpressionFactory(); + } + + /** + * Register the server with this Ebean singleton. Specify if the registered + * server is the primary/default server. + */ + protected static void register(EbeanServer server, boolean isPrimaryServer) { + serverMgr.register(server, isPrimaryServer); + } + + /** + * Return the next identity value for a given bean type. + *

+ * This will only work when a IdGenerator is on this bean type such as a DB + * sequence or UUID. + *

+ *

+ * For DB's supporting getGeneratedKeys and sequences such as Oracle10 you do + * not need to use this method generally. It is made available for more + * complex cases where it is useful to get an ID prior to some processing. + *

+ */ + public static Object nextId(Class beanType) { + return serverMgr.getPrimaryServer().nextId(beanType); + } + + /** + * Start a new explicit transaction. + *

+ * The transaction is stored in a ThreadLocal variable and typically you only + * need to use the returned Transaction IF you wish to do things like + * use batch mode, change the transaction isolation level, use savepoints or + * log comments to the transaction log. + *

+ *

+ * Example of using a transaction to span multiple calls to find(), save() + * etc. + *

+ * + *
+   * // start a transaction (stored in a ThreadLocal)
+   * Ebean.beginTransaction(); 
+   * try { 
+   * 	Order order = Ebean.find(Order.class,10); ...
+   * 
+   * 	Ebean.save(order);
+   * 
+   * 	Ebean.commitTransaction();
+   * 
+   * } finally { 
+   * 	// rollback if we didn't commit 
+   * 	// i.e. an exception occurred before commitTransaction(). 
+   * 	Ebean.endTransaction(); 
+   * }
+   * 
+ * + *

+ * If you want to externalise the transaction management then you should be + * able to do this via EbeanServer. Specifically with EbeanServer you can pass + * the transaction to the various find() and save() execute() methods. This + * gives you the ability to create the transactions yourself externally from + * Ebean and pass those transactions through to the various methods available + * on EbeanServer. + *

+ */ + public static Transaction beginTransaction() { + return serverMgr.getPrimaryServer().beginTransaction(); + } + + /** + * Start a transaction additionally specifying the isolation level. + * + * @param isolation + * the Transaction isolation level + * + */ + public static Transaction beginTransaction(TxIsolation isolation) { + return serverMgr.getPrimaryServer().beginTransaction(isolation); + } + + /** + * Returns the current transaction or null if there is no current transaction + * in scope. + */ + public static Transaction currentTransaction() { + return serverMgr.getPrimaryServer().currentTransaction(); + } + + /** + * Commit the current transaction. + */ + public static void commitTransaction() { + serverMgr.getPrimaryServer().commitTransaction(); + } + + /** + * Rollback the current transaction. + */ + public static void rollbackTransaction() { + serverMgr.getPrimaryServer().rollbackTransaction(); + } + + /** + * If the current transaction has already been committed do nothing otherwise + * rollback the transaction. + *

+ * Useful to put in a finally block to ensure the transaction is ended, rather + * than a rollbackTransaction() in each catch block. + *

+ *

+ * Code example: + *

+ * + *
+   * Ebean.beginTransaction();
+   * try {
+   *   // do some fetching and or persisting
+   *   // commit at the end Ebean.commitTransaction();
+   * 
+   * } finally {
+   *   // if commit didn't occur then rollback the transaction
+   *   Ebean.endTransaction();
+   * }
+   * 
+ */ + public static void endTransaction() { + serverMgr.getPrimaryServer().endTransaction(); + } + + /** + * Return a map of the differences between two objects of the same type. + *

+ * When null is passed in for b, then the 'OldValues' of a is used for the + * difference comparison. + *

+ */ + public static Map diff(Object a, Object b) { + return serverMgr.getPrimaryServer().diff(a, b); + } + + /** + * Either Insert or Update the bean depending on its state. + *

+ * If there is no current transaction one will be created and committed for + * you automatically. + *

+ *

+ * Save can cascade along relationships. For this to happen you need to + * specify a cascade of CascadeType.ALL or CascadeType.PERSIST on the + * OneToMany, OneToOne or ManyToMany annotation. + *

+ *

+ * In this example below the details property has a CascadeType.ALL set so + * saving an order will also save all its details. + *

+ * + *
+   * public class Order { ...
+   * 	
+   * 	@OneToMany(cascade=CascadeType.ALL, mappedBy="order")
+   * 	@JoinColumn(name="order_id") 
+   * 	List<OrderDetail> details; 
+   * 	... 
+   * }
+   * 
+ * + *

+ * When a save cascades via a OneToMany or ManyToMany Ebean will automatically + * set the 'parent' object to the 'detail' object. In the example below in + * saving the order and cascade saving the order details the 'parent' order + * will be set against each order detail when it is saved. + *

+ */ + public static void save(Object bean) throws OptimisticLockException { + serverMgr.getPrimaryServer().save(bean); + } + + /** + * Force an update using the bean updating the non-null properties. + *

+ * You can use this method to FORCE an update to occur (even on a bean that + * has not been fetched but say built from JSON or XML). When + * {@link Ebean#save(Object)} is used Ebean determines whether to use an + * insert or an update based on the state of the bean. Using this method will + * force an update to occur. + *

+ *

+ * It is expected that this method is most useful in stateless REST services + * or web applications where you have the values you wish to update but no + * existing bean. + *

+ *

+ * For updates against beans that have not been fetched (say built from JSON + * or XML) this will treat deleteMissingChildren=true and will delete any + * 'missing children'. Refer to + * {@link EbeanServer#update(Object, Set, Transaction, boolean, boolean)}. + *

+ * + *
+   * 
+   * Customer c = new Customer();
+   * c.setId(7);
+   * c.setName("ModifiedNameNoOCC");
+   * 
+   * // generally you should set the version property
+   * // so that Optimistic Concurrency Checking is used.
+   * // If a version property is not set then no Optimistic
+   * // Concurrency Checking occurs for the update
+   * // c.setLastUpdate(lastUpdateTime);
+   * 
+   * // by default the Non-null properties
+   * // are included in the update
+   * Ebean.update(c);
+   * 
+   * 
+ */ + public static void update(Object bean) { + serverMgr.getPrimaryServer().update(bean); + } + + /** + * Force an update using the bean explicitly stating the properties to update. + *

+ * If you don't specify explicit properties to use in the update then the + * non-null properties are included in the update. + *

+ *

+ * For updates against beans that have not been fetched (say built from JSON + * or XML) this will treat deleteMissingChildren=true and will delete any + * 'missing children'. Refer to + * {@link EbeanServer#update(Object, Set, Transaction, boolean, boolean)}. + *

+ * + * @param bean + * The bean holding the values to be included in the update. + * @param updateProps + * the explicit set of properties to include in the update (can be + * null). + */ + public static void update(Object bean, Set updateProps) { + serverMgr.getPrimaryServer().update(bean, updateProps); + } + + /** + * Save all the beans from an Iterator. + */ + public static int save(Iterator iterator) throws OptimisticLockException { + return serverMgr.getPrimaryServer().save(iterator); + } + + /** + * Save all the beans from a Collection. + */ + public static int save(Collection c) throws OptimisticLockException { + return save(c.iterator()); + } + + /** + * Delete the associations (from the intersection table) of a ManyToMany given + * the owner bean and the propertyName of the ManyToMany collection. + *

+ * Typically these deletions occur automatically when persisting a ManyToMany + * collection and this provides a way to invoke those deletions directly. + *

+ * + * @return the number of associations deleted (from the intersection table). + */ + public static int deleteManyToManyAssociations(Object ownerBean, String propertyName) { + return serverMgr.getPrimaryServer().deleteManyToManyAssociations(ownerBean, propertyName); + } + + /** + * Save the associations of a ManyToMany given the owner bean and the + * propertyName of the ManyToMany collection. + *

+ * Typically the saving of these associations (inserting into the intersection + * table) occurs automatically when persisting a ManyToMany. This provides a + * way to invoke those insertions directly. + *

+ *

+ * You can use this when the collection is new and in this case all the + * entries in the collection are treated as additions are result in inserts + * into the intersection table. + *

+ */ + public static void saveManyToManyAssociations(Object ownerBean, String propertyName) { + serverMgr.getPrimaryServer().saveManyToManyAssociations(ownerBean, propertyName); + } + + /** + * Save the associated collection or bean given the property name. + *

+ * This is similar to performing a save cascade on a specific property + * manually/programmatically. + *

+ *

+ * Note that you can turn on/off cascading for a transaction via + * {@link Transaction#setPersistCascade(boolean)} + *

+ * + * @param ownerBean + * the bean instance holding the property we want to save + * @param propertyName + * the property we want to save + */ + public static void saveAssociation(Object ownerBean, String propertyName) { + serverMgr.getPrimaryServer().saveAssociation(ownerBean, propertyName); + } + + /** + * Delete the bean. + *

+ * If there is no current transaction one will be created and committed for + * you automatically. + *

+ */ + public static void delete(Object bean) throws OptimisticLockException { + serverMgr.getPrimaryServer().delete(bean); + } + + /** + * Delete the bean given its type and id. + */ + public static int delete(Class beanType, Object id) { + return serverMgr.getPrimaryServer().delete(beanType, id); + } + + /** + * Delete several beans given their type and id values. + */ + public static void delete(Class beanType, Collection ids) { + serverMgr.getPrimaryServer().delete(beanType, ids); + } + + /** + * Delete all the beans from an Iterator. + */ + public static int delete(Iterator it) throws OptimisticLockException { + return serverMgr.getPrimaryServer().delete(it); + } + + /** + * Delete all the beans from a Collection. + */ + public static int delete(Collection c) throws OptimisticLockException { + return delete(c.iterator()); + } + + /** + * Refresh the values of a bean. + *

+ * Note that this does not refresh any OneToMany or ManyToMany properties. + *

+ */ + public static void refresh(Object bean) { + serverMgr.getPrimaryServer().refresh(bean); + } + + /** + * Refresh a 'many' property of a bean. + * + *
+   * Order order = ...;
+   * ...
+   * // refresh the order details...
+   * Ebean.refreshMany(order, "details");
+   * 
+ * + * @param bean + * the entity bean containing the List Set or Map to refresh. + * @param manyPropertyName + * the property name of the List Set or Map to refresh. + */ + public static void refreshMany(Object bean, String manyPropertyName) { + serverMgr.getPrimaryServer().refreshMany(bean, manyPropertyName); + } + + /** + * Get a reference object. + *

+ * This is sometimes described as a proxy (with lazy loading). + *

+ * + *
+   * Product product = Ebean.getReference(Product.class, 1);
+   * 
+   * // You can get the id without causing a fetch/lazy load
+   * Integer productId = product.getId();
+   * 
+   * // If you try to get any other property a fetch/lazy loading will occur
+   * // This will cause a query to execute...
+   * String name = product.getName();
+   * 
+ * + * @param beanType + * the type of entity bean + * @param id + * the id value + */ + public static T getReference(Class beanType, Object id) { + return serverMgr.getPrimaryServer().getReference(beanType, id); + } + + /** + * Sort the list using the sortByClause which can contain a comma delimited + * list of property names and keywords asc, desc, nullsHigh and nullsLow. + *
    + *
  • asc - ascending order (which is the default)
  • + *
  • desc - Descending order
  • + *
  • nullsHigh - Treat null values as high/large values (which is the + * default)
  • + *
  • nullsLow- Treat null values as low/very small values
  • + *
+ *

+ * If you leave off any keywords the defaults are ascending order and treating + * nulls as high values. + *

+ *

+ * Note that the sorting uses a Comparator and Collections.sort(); and does + * not invoke a DB query. + *

+ * + *
+   * 
+   * // find orders and their customers
+   * List<Order> list = Ebean.find(Order.class)
+   *     .fetch("customer")
+   *     .orderBy("id")
+   *     .findList();
+   * 
+   * // sort by customer name ascending, then by order shipDate
+   * // ... then by the order status descending
+   * Ebean.sort(list, "customer.name, shipDate, status desc");
+   * 
+   * // sort by customer name descending (with nulls low)
+   * // ... then by the order id
+   * Ebean.sort(list, "customer.name desc nullsLow, id");
+   * 
+   * 
+ * + * @param list + * the list of entity beans + * @param sortByClause + * the properties to sort the list by + */ + public static void sort(List list, String sortByClause) { + serverMgr.getPrimaryServer().sort(list, sortByClause); + } + + /** + * Find a bean using its unique id. This will not use caching. + * + *
+   * // Fetch order 1
+   * Order order = Ebean.find(Order.class, 1);
+   * 
+ * + *

+ * If you want more control over the query then you can use createQuery() and + * Query.findUnique(); + *

+ * + *
+   * // ... additionally fetching customer, customer shipping address,
+   * // order details, and the product associated with each order detail.
+   * // note: only product id and name is fetch (its a "partial object").
+   * // note: all other objects use "*" and have all their properties fetched.
+   * 
+   * Query<Order> query = Ebean.createQuery(Order.class);
+   * query.setId(1);
+   * query.fetch("customer");
+   * query.fetch("customer.shippingAddress");
+   * query.fetch("details");
+   * 
+   * // fetch associated products but only fetch their product id and name
+   * query.fetch("details.product", "name");
+   * 
+   * // traverse the object graph...
+   * 
+   * Order order = query.findUnique();
+   * Customer customer = order.getCustomer();
+   * Address shippingAddress = customer.getShippingAddress();
+   * List<OrderDetail> details = order.getDetails();
+   * OrderDetail detail0 = details.get(0);
+   * Product product = detail0.getProduct();
+   * String productName = product.getName();
+   * 
+ * + * @param beanType + * the type of entity bean to fetch + * @param id + * the id value + */ + public static T find(Class beanType, Object id) { + return serverMgr.getPrimaryServer().find(beanType, id); + } + + /** + * Create a SqlQuery for executing native sql + * query statements. + *

+ * Note that you can use raw SQL with entity beans, refer to the SqlSelect + * annotation for examples. + *

+ */ + public static SqlQuery createSqlQuery(String sql) { + return serverMgr.getPrimaryServer().createSqlQuery(sql); + } + + /** + * Create a named sql query. + *

+ * The query statement will be defined in a deployment orm xml file. + *

+ * + * @param namedQuery + * the name of the query + */ + public static SqlQuery createNamedSqlQuery(String namedQuery) { + return serverMgr.getPrimaryServer().createNamedSqlQuery(namedQuery); + } + + /** + * Create a sql update for executing native dml statements. + *

+ * Use this to execute a Insert Update or Delete statement. The statement will + * be native to the database and contain database table and column names. + *

+ *

+ * See {@link SqlUpdate} for example usage. + *

+ *

+ * Where possible it would be expected practice to put the statement in a orm + * xml file (named update) and use {@link #createNamedSqlUpdate(String)} . + *

+ */ + public static SqlUpdate createSqlUpdate(String sql) { + return serverMgr.getPrimaryServer().createSqlUpdate(sql); + } + + /** + * Create a CallableSql to execute a given stored procedure. + * + * @see CallableSql + */ + public static CallableSql createCallableSql(String sql) { + return serverMgr.getPrimaryServer().createCallableSql(sql); + } + + /** + * Create a named sql update. + *

+ * The statement (an Insert Update or Delete statement) will be defined in a + * deployment orm xml file. + *

+ * + *
+   * // Use a namedQuery
+   * UpdateSql update = Ebean.createNamedSqlUpdate("update.topic.count");
+   * 
+   * update.setParameter("count", 1);
+   * update.setParameter("topicId", 50);
+   * 
+   * int modifiedCount = update.execute();
+   * 
+ */ + public static SqlUpdate createNamedSqlUpdate(String namedQuery) { + return serverMgr.getPrimaryServer().createNamedSqlUpdate(namedQuery); + } + + /** + * Return a named Query that will have defined fetch paths, predicates etc. + *

+ * The query is created from a statement that will be defined in a deployment + * orm xml file or NamedQuery annotations. The query will typically already + * define fetch paths, predicates, order by clauses etc so often you will just + * need to bind required parameters and then execute the query. + *

+ * + *
+   * // example
+   * Query<Order> query = Ebean.createNamedQuery(Order.class, "new.for.customer");
+   * query.setParameter("customerId", 23);
+   * List<Order> newOrders = query.findList();
+   * 
+ * + * @param beanType + * the class of entity to be fetched + * @param namedQuery + * the name of the query + */ + public static Query createNamedQuery(Class beanType, String namedQuery) { + + return serverMgr.getPrimaryServer().createNamedQuery(beanType, namedQuery); + } + + /** + * Create a query using the query language. + *

+ * Note that you are allowed to add additional clauses using where() as well + * as use fetch() and setOrderBy() after the query has been created. + *

+ *

+ * Note that this method signature used to map to named queries and that has + * moved to {@link #createNamedQuery(Class, String)}. + *

+ * + *
+   * 
+   * String q = "find order fetch details where status = :st";
+   * 
+   * List<Order> newOrders = Ebean.createQuery(Order.class, q)
+   *     .setParameter("st", Order.Status.NEW)
+   *     .findList();
+   * 
+ * + * @param query + * the object query + */ + public static Query createQuery(Class beanType, String query) { + return serverMgr.getPrimaryServer().createQuery(beanType, query); + } + + /** + * Create a named orm update. The update statement is specified via the + * NamedUpdate annotation. + *

+ * The orm update differs from the SqlUpdate in that it uses the bean name and + * bean property names rather than table and column names. + *

+ *

+ * Note that named update statements can be specified in raw sql (with column + * and table names) or using bean name and bean property names. This can be + * specified with the isSql flag. + *

+ *

+ * Example named updates: + *

+ * + *
+   * package app.data;
+   * 
+   * import ...
+   * 
+   * @NamedUpdates(value = { 
+   * 	@NamedUpdate( name = "setTitle", 
+   * 		isSql = false, 
+   * 		notifyCache = false, 
+   * 		update = "update topic set title = :title, postCount = :postCount where id = :id"), 
+   * 	@NamedUpdate( name = "setPostCount",
+   * 		notifyCache = false,
+   * 		update = "update f_topic set post_count = :postCount where id = :id"), 
+   * 	@NamedUpdate( name = "incrementPostCount", 
+   * 		notifyCache = false, 
+   * 		isSql = false,
+   * 		update = "update Topic set postCount = postCount + 1 where id = :id") }) 
+   * @Entity 
+   * @Table(name = "f_topic") 
+   * public class Topic { ...
+   * 
+ * + *

+ * Example using a named update: + *

+ * + *
+   * Update<Topic> update = Ebean.createNamedUpdate(Topic.class, "setPostCount");
+   * update.setParameter("postCount", 10);
+   * update.setParameter("id", 3);
+   * 
+   * int rows = update.execute();
+   * System.out.println("rows updated: " + rows);
+   * 
+ */ + public static Update createNamedUpdate(Class beanType, String namedUpdate) { + + return serverMgr.getPrimaryServer().createNamedUpdate(beanType, namedUpdate); + } + + /** + * Create a orm update where you will supply the insert/update or delete + * statement (rather than using a named one that is already defined using the + * @NamedUpdates annotation). + *

+ * The orm update differs from the sql update in that it you can use the bean + * name and bean property names rather than table and column names. + *

+ *

+ * An example: + *

+ * + *
+   * 
+   * // The bean name and properties - "topic","postCount" and "id"
+   * 
+   * // will be converted into their associated table and column names
+   * String updStatement = "update topic set postCount = :pc where id = :id";
+   * 
+   * Update<Topic> update = Ebean.createUpdate(Topic.class, updStatement);
+   * 
+   * update.set("pc", 9);
+   * update.set("id", 3);
+   * 
+   * int rows = update.execute();
+   * System.out.println("rows updated:" + rows);
+   * 
+ */ + public static Update createUpdate(Class beanType, String ormUpdate) { + + return serverMgr.getPrimaryServer().createUpdate(beanType, ormUpdate); + } + + /** + * Create a CsvReader for a given beanType. + */ + public static CsvReader createCsvReader(Class beanType) { + + return serverMgr.getPrimaryServer().createCsvReader(beanType); + } + + /** + * Create a query for a type of entity bean. + *

+ * You can use the methods on the Query object to specify fetch paths, + * predicates, order by, limits etc. + *

+ *

+ * You then use findList(), findSet(), findMap() and findUnique() to execute + * the query and return the collection or bean. + *

+ *

+ * Note that a query executed by {@link Query#findList()} + * {@link Query#findSet()} etc will execute against the same EbeanServer from + * which is was created. + *

+ * + *
+   * // Find order 2 additionally fetching the customer, details and details.product
+   * // name.
+   * 
+   * Query<Order> query = Ebean.createQuery(Order.class);
+   * query.fetch("customer");
+   * query.fetch("details");
+   * query.fetch("detail.product", "name");
+   * query.setId(2);
+   * 
+   * Order order = query.findUnique();
+   * 
+   * // Find order 2 additionally fetching the customer, details and details.product
+   * // name.
+   * // Note: same query as above but using the query language
+   * // Note: using a named query would be preferred practice
+   * 
+   * String oql = "find order fetch customer fetch details fetch details.product (name) where id = :orderId ";
+   * 
+   * Query<Order> query = Ebean.createQuery(Order.class);
+   * query.setQuery(oql);
+   * query.setParameter("orderId", 2);
+   * 
+   * Order order = query.findUnique();
+   * 
+   * // Using a named query
+   * Query<Order> query = Ebean.createQuery(Order.class, "with.details");
+   * query.setParameter("orderId", 2);
+   * 
+   * Order order = query.findUnique();
+   * 
+   * 
+ * + * @param beanType + * the class of entity to be fetched + * @return A ORM Query object for this beanType + */ + public static Query createQuery(Class beanType) { + + return serverMgr.getPrimaryServer().createQuery(beanType); + } + + /** + * Create a query for a type of entity bean. + *

+ * This is actually the same as {@link #createQuery(Class)}. The reason it + * exists is that people used to JPA will probably be looking for a + * createQuery method (the same as entityManager). + *

+ * + * @param beanType + * the type of entity bean to find + * @return A ORM Query object for this beanType + */ + public static Query find(Class beanType) { + + return serverMgr.getPrimaryServer().find(beanType); + } + + /** + * Create a filter for sorting and filtering lists of entities locally without + * going back to the database. + *

+ * This produces and returns a new list with the sort and filters applied. + *

+ *

+ * Refer to {@link Filter} for an example of its use. + *

+ */ + public static Filter filter(Class beanType) { + return serverMgr.getPrimaryServer().filter(beanType); + } + + /** + * Execute a Sql Update Delete or Insert statement. This returns the number of + * rows that where updated, deleted or inserted. If is executed in batch then + * this returns -1. You can get the actual rowCount after commit() from + * updateSql.getRowCount(). + *

+ * If you wish to execute a Sql Select natively then you should use the + * FindByNativeSql object. + *

+ *

+ * Note that the table modification information is automatically deduced and + * you do not need to call the Ebean.externalModification() method when you + * use this method. + *

+ *

+ * Example: + *

+ * + *
+   * // example that uses 'named' parameters 
+   * String s = "UPDATE f_topic set post_count = :count where id = :id"
+   * 
+   * SqlUpdate update = Ebean.createSqlUpdate(s);
+   * 
+   * update.setParameter("id", 1);
+   * update.setParameter("count", 50);
+   * 
+   * int modifiedCount = Ebean.execute(update);
+   * 
+   * String msg = "There where " + modifiedCount + "rows updated";
+   * 
+ * + * @param sqlUpdate + * the update sql potentially with bind values + * + * @return the number of rows updated or deleted. -1 if executed in batch. + * + * @see SqlUpdate + * @see CallableSql + * @see Ebean#execute(CallableSql) + */ + public static int execute(SqlUpdate sqlUpdate) { + return serverMgr.getPrimaryServer().execute(sqlUpdate); + } + + /** + * For making calls to stored procedures. + *

+ * Example: + *

+ * + *
+   * String sql = "{call sp_order_modify(?,?,?)}";
+   * 
+   * CallableSql cs = Ebean.createCallableSql(sql);
+   * cs.setParameter(1, 27);
+   * cs.setParameter(2, "SHIPPED");
+   * cs.registerOut(3, Types.INTEGER);
+   * 
+   * Ebean.execute(cs);
+   * 
+   * // read the out parameter
+   * Integer returnValue = (Integer) cs.getObject(3);
+   * 
+ * + * @see CallableSql + * @see Ebean#execute(SqlUpdate) + */ + public static int execute(CallableSql callableSql) { + return serverMgr.getPrimaryServer().execute(callableSql); + } + + /** + * Execute a TxRunnable in a Transaction with an explicit scope. + *

+ * The scope can control the transaction type, isolation and rollback + * semantics. + *

+ * + *
+   * // set specific transactional scope settings 
+   * TxScope scope = TxScope.requiresNew().setIsolation(TxIsolation.SERIALIZABLE);
+   * 
+   * Ebean.execute(scope, new TxRunnable() { 
+   * 	public void run() { 
+   * 		User u1 = Ebean.find(User.class, 1); 
+   * 		...
+   * 
+   * 	} 
+   * });
+   * 
+ */ + public static void execute(TxScope scope, TxRunnable r) { + serverMgr.getPrimaryServer().execute(scope, r); + } + + /** + * Execute a TxRunnable in a Transaction with the default scope. + *

+ * The default scope runs with REQUIRED and by default will rollback on any + * exception (checked or runtime). + *

+ * + *
+   * Ebean.execute(new TxRunnable() {
+   *   public void run() {
+   *     User u1 = Ebean.find(User.class, 1);
+   *     User u2 = Ebean.find(User.class, 2);
+   * 
+   *     u1.setName("u1 mod");
+   *     u2.setName("u2 mod");
+   * 
+   *     Ebean.save(u1);
+   *     Ebean.save(u2);
+   *   }
+   * });
+   * 
+ */ + public static void execute(TxRunnable r) { + serverMgr.getPrimaryServer().execute(r); + } + + /** + * Execute a TxCallable in a Transaction with an explicit scope. + *

+ * The scope can control the transaction type, isolation and rollback + * semantics. + *

+ * + *
+   * // set specific transactional scope settings 
+   * TxScope scope = TxScope.requiresNew().setIsolation(TxIsolation.SERIALIZABLE);
+   * 
+   * Ebean.execute(scope, new TxCallable<String>() {
+   * 	public String call() { 
+   * 		User u1 = Ebean.find(User.class, 1); 
+   * 		...
+   * 		return u1.getEmail(); 
+   * 	} 
+   * });
+   * 
+ * + */ + public static T execute(TxScope scope, TxCallable c) { + return serverMgr.getPrimaryServer().execute(scope, c); + } + + /** + * Execute a TxCallable in a Transaction with the default scope. + *

+ * The default scope runs with REQUIRED and by default will rollback on any + * exception (checked or runtime). + *

+ *

+ * This is basically the same as TxRunnable except that it returns an Object + * (and you specify the return type via generics). + *

+ * + *
+   * Ebean.execute(new TxCallable<String>() {
+   *   public String call() {
+   *     User u1 = Ebean.find(User.class, 1);
+   *     User u2 = Ebean.find(User.class, 2);
+   * 
+   *     u1.setName("u1 mod");
+   *     u2.setName("u2 mod");
+   * 
+   *     Ebean.save(u1);
+   *     Ebean.save(u2);
+   * 
+   *     return u1.getEmail();
+   *   }
+   * });
+   * 
+ */ + public static T execute(TxCallable c) { + return serverMgr.getPrimaryServer().execute(c); + } + + /** + * Inform Ebean that tables have been modified externally. These could be the + * result of from calling a stored procedure, other JDBC calls or external + * programs including other frameworks. + *

+ * If you use Ebean.execute(UpdateSql) then the table modification information + * is automatically deduced and you do not need to call this method yourself. + *

+ *

+ * This information is used to invalidate objects out of the cache and + * potentially text indexes. This information is also automatically broadcast + * across the cluster. + *

+ *

+ * If there is a transaction then this information is placed into the current + * transactions event information. When the transaction is commited this + * information is registered (with the transaction manager). If this + * transaction is rolled back then none of the transaction event information + * registers including the information you put in via this method. + *

+ *

+ * If there is NO current transaction when you call this method then this + * information is registered immediately (with the transaction manager). + *

+ * + * @param tableName + * the name of the table that was modified + * @param inserts + * true if rows where inserted into the table + * @param updates + * true if rows on the table where updated + * @param deletes + * true if rows on the table where deleted + */ + public static void externalModification(String tableName, boolean inserts, boolean updates, + boolean deletes) { + + serverMgr.getPrimaryServer().externalModification(tableName, inserts, updates, deletes); + } + + /** + * Return the BeanState for a given entity bean. + *

+ * This will return null if the bean is not an enhanced (or subclassed) entity + * bean. + *

+ */ + public static BeanState getBeanState(Object bean) { + return serverMgr.getPrimaryServer().getBeanState(bean); + } + + /** + * Return the manager of the server cache ("L2" cache). + * + */ + public static ServerCacheManager getServerCacheManager() { + return serverMgr.getPrimaryServer().getServerCacheManager(); + } + + /** + * Return the BackgroundExecutor service for asynchronous processing of + * queries. + */ + public static BackgroundExecutor getBackgroundExecutor() { + return serverMgr.getPrimaryServer().getBackgroundExecutor(); + } + + /** + * Run the cache warming queries on all bean types that have one defined for + * the default/primary EbeanServer. + *

+ * A cache warming query can be defined via {@link CacheStrategy}. + *

+ */ + public static void runCacheWarming() { + serverMgr.getPrimaryServer().runCacheWarming(); + } + + /** + * Run the cache warming query for a specific bean type for the + * default/primary EbeanServer. + *

+ * A cache warming query can be defined via {@link CacheStrategy}. + *

+ */ + public static void runCacheWarming(Class beanType) { + + serverMgr.getPrimaryServer().runCacheWarming(beanType); + } + + /** + * Create a JsonContext that will use the default configuration options. + */ + public static JsonContext createJsonContext() { + return serverMgr.getPrimaryServer().createJsonContext(); + } + +} diff --git a/src/main/java/com/avaje/ebean/EbeanServer.java b/src/main/java/com/avaje/ebean/EbeanServer.java new file mode 100644 index 000000000..e31240efe --- /dev/null +++ b/src/main/java/com/avaje/ebean/EbeanServer.java @@ -0,0 +1,1154 @@ +package com.avaje.ebean; + +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.persistence.OptimisticLockException; + +import com.avaje.ebean.annotation.CacheStrategy; +import com.avaje.ebean.cache.ServerCacheManager; +import com.avaje.ebean.config.ServerConfig; +import com.avaje.ebean.text.csv.CsvReader; +import com.avaje.ebean.text.json.JsonContext; + +/** + * Provides the API for fetching and saving beans to a particular DataSource. + *

+ * Registration with the Ebean Singleton:
+ * When a EbeanServer is constructed it can be registered with the Ebean + * singleton (see {@link ServerConfig#setRegister(boolean)}). The Ebean + * singleton is essentially a map of EbeanServer's that have been registered + * with it. The EbeanServer can then be retrieved later via + * {@link Ebean#getServer(String)}. + *

+ *

+ * The 'default' EbeanServer
+ * One EbeanServer can be designated as the 'default' or 'primary' EbeanServer + * (see {@link ServerConfig#setDefaultServer(boolean)}. Many methods on Ebean + * such as {@link Ebean#find(Class)} etc are actually just a convenient way to + * call methods on the 'default/primary' EbeanServer. This is handy for + * applications that use a single DataSource. + *

+ * There is one EbeanServer per Database (javax.sql.DataSource). One EbeanServer + * is referred to as the 'default' server and that is the one that + * Ebean methods such as {@link Ebean#find(Class)} use. + *

+ *

+ * Constructing a EbeanServer
+ * EbeanServer's are constructed by the EbeanServerFactory. They can be created + * programmatically via {@link EbeanServerFactory#create(ServerConfig)} or they + * can be automatically constructed on demand using configuration information in + * the ebean.properties file. + *

+ *

+ * Example: Get a EbeanServer + *

+ * + *
+ * // Get access to the Human Resources EbeanServer/Database
+ * EbeanServer hrServer = Ebean.getServer("HR");
+ * 
+ * 
+ * // fetch contact 3 from the HR database Contact contact =
+ * hrServer.find(Contact.class, new Integer(3));
+ * 
+ * contact.setStatus("INACTIVE"); ...
+ * 
+ * // save the contact back to the HR database hrServer.save(contact);
+ * 
+ * + *

+ * EbeanServer has more API than Ebean
+ * EbeanServer provides additional API compared with Ebean. For example it + * provides more control over the use of Transactions that is not available in + * the Ebean API. + *

+ *

+ * External Transactions: If you wanted to use transactions created + * externally to eBean then EbeanServer provides additional methods where you + * can explicitly pass a transaction (that can be created externally). + *

+ *

+ * Bypass ThreadLocal Mechanism: If you want to bypass the built in + * ThreadLocal transaction management you can use the createTransaction() + * method. Example: a single thread requires more than one transaction. + *

+ * + * @see Ebean + * @see EbeanServerFactory + * @see ServerConfig + */ +public interface EbeanServer { + + /** + * Return the AdminAutofetch which is used to control and configure the + * Autofetch service at runtime. + */ + public AdminAutofetch getAdminAutofetch(); + + /** + * Return the name. This is used with {@link Ebean#getServer(String)} to get a + * EbeanServer that was registered with the Ebean singleton. + */ + public String getName(); + + /** + * Return the ExpressionFactory for this server. + */ + public ExpressionFactory getExpressionFactory(); + + /** + * Return the BeanState for a given entity bean. + *

+ * This will return null if the bean is not an enhanced (or subclassed) entity + * bean. + *

+ */ + public BeanState getBeanState(Object bean); + + /** + * Return the value of the Id property for a given bean. + */ + public Object getBeanId(Object bean); + + /** + * Return a map of the differences between two objects of the same type. + *

+ * When null is passed in for b, then the 'OldValues' of a is used for the + * difference comparison. + *

+ */ + public Map diff(Object a, Object b); + + /** + * Create a new instance of T that is an EntityBean (for subclassing). + *

+ * Note that if you are using enhancement (rather than subclassing) then you + * do not need to use this method and just new up a bean. + *

+ *

+ * Potentially useful when using subclassing and you wish to programmatically + * load a entity bean . Otherwise this method is generally not required. + *

+ */ + public T createEntityBean(Class type); + + /** + * Create a ObjectInputStream that can be used to deserialise "Proxy" or + * "SubClassed" entity beans. + *

+ * This is NOT required when entity beans are "Enhanced" (via java agent or + * ant task etc). + *

+ *

+ * The reason this is needed to deserialise "Proxy" beans is because Ebean + * creates the "Proxy/SubClass" classes in a class loader - and generally the + * class loader deserialising the inputStream is not aware of these other + * classes. + *

+ */ + public ObjectInputStream createProxyObjectInputStream(InputStream is); + + /** + * Create a CsvReader for a given beanType. + */ + public CsvReader createCsvReader(Class beanType); + + /** + * Create a named query for an entity bean (refer + * {@link Ebean#createQuery(Class, String)}) + *

+ * The query statement will be defined in a deployment orm xml file. + *

+ * + * @see Ebean#createQuery(Class, String) + */ + public Query createNamedQuery(Class beanType, String namedQuery); + + /** + * Create a query using the query language. + *

+ * Note that you are allowed to add additional clauses using where() as well + * as use fetch() and setOrderBy() after the query has been created. + *

+ *

+ * Note that this method signature used to map to named queries and that has + * moved to {@link #createNamedQuery(Class, String)}. + *

+ * + *
+   *  EbeanServer ebeanServer = ... ;
+   *  String q = "find order fetch details where status = :st";
+   *  
+   *  List<Order> newOrders 
+   *        = ebeanServer.createQuery(Order.class, q)
+   *             .setParameter("st", Order.Status.NEW)
+   *             .findList();
+   * 
+ * + * @param query + * the object query + */ + public Query createQuery(Class beanType, String query); + + /** + * Create a query for an entity bean (refer {@link Ebean#createQuery(Class)} + * ). + * + * @see Ebean#createQuery(Class) + */ + public Query createQuery(Class beanType); + + /** + * Create a query for a type of entity bean (the same as + * {@link EbeanServer#createQuery(Class)}). + */ + public Query find(Class beanType); + + /** + * Return the next unique identity value for a given bean type. + *

+ * This will only work when a IdGenerator is on the bean such as for beans + * that use a DB sequence or UUID. + *

+ *

+ * For DB's supporting getGeneratedKeys and sequences such as Oracle10 you do + * not need to use this method generally. It is made available for more + * complex cases where it is useful to get an ID prior to some processing. + *

+ */ + public Object nextId(Class beanType); + + /** + * Create a filter for filtering lists of entity beans. + */ + public Filter filter(Class beanType); + + /** + * Sort the list using the sortByClause. + * + * @see Ebean#sort(List, String) + * + * @param list + * the list of entity beans + * @param sortByClause + * the properties to sort the list by + */ + public void sort(List list, String sortByClause); + + /** + * Create a named update for an entity bean (refer + * {@link Ebean#createNamedUpdate(Class, String)}). + */ + public Update createNamedUpdate(Class beanType, String namedUpdate); + + /** + * Create a update for an entity bean where you will manually specify the + * insert update or delete statement. + */ + public Update createUpdate(Class beanType, String ormUpdate); + + /** + * Create a sql query for executing native sql query statements (refer + * {@link Ebean#createSqlQuery(String)}). + * + * @see Ebean#createSqlQuery(String) + */ + public SqlQuery createSqlQuery(String sql); + + /** + * Create a named sql query (refer {@link Ebean#createNamedSqlQuery(String)} + * ). + *

+ * The query statement will be defined in a deployment orm xml file. + *

+ * + * @see Ebean#createNamedSqlQuery(String) + */ + public SqlQuery createNamedSqlQuery(String namedQuery); + + /** + * Create a sql update for executing native dml statements (refer + * {@link Ebean#createSqlUpdate(String)}). + * + * @see Ebean#createSqlUpdate(String) + */ + public SqlUpdate createSqlUpdate(String sql); + + /** + * Create a CallableSql to execute a given stored procedure. + */ + public CallableSql createCallableSql(String callableSql); + + /** + * Create a named sql update (refer {@link Ebean#createNamedSqlUpdate(String)} + * ). + *

+ * The statement (an Insert Update or Delete statement) will be defined in a + * deployment orm xml file. + *

+ * + * @see Ebean#createNamedSqlUpdate(String) + */ + public SqlUpdate createNamedSqlUpdate(String namedQuery); + + /** + * Create a new transaction that is not held in TransactionThreadLocal. + *

+ * You will want to do this if you want multiple Transactions in a single + * thread or generally use transactions outside of the TransactionThreadLocal + * management. + *

+ */ + public Transaction createTransaction(); + + /** + * Create a new transaction additionally specifying the isolation level. + *

+ * Note that this transaction is NOT stored in a thread local. + *

+ */ + public Transaction createTransaction(TxIsolation isolation); + + /** + * Start a new transaction putting it into a ThreadLocal. + * + * @see Ebean#beginTransaction() + */ + public Transaction beginTransaction(); + + /** + * Start a transaction additionally specifying the isolation level. + */ + public Transaction beginTransaction(TxIsolation isolation); + + /** + * Returns the current transaction or null if there is no current transaction + * in scope. + */ + public Transaction currentTransaction(); + + /** + * Commit the current transaction. + * + * @see Ebean#commitTransaction() + */ + public void commitTransaction(); + + /** + * Rollback the current transaction. + * + * @see Ebean#rollbackTransaction() + */ + public void rollbackTransaction(); + + /** + * If the current transaction has already been committed do nothing otherwise + * rollback the transaction. + *

+ * Useful to put in a finally block to ensure the transaction is ended, rather + * than a rollbackTransaction() in each catch block. + *

+ *

+ * Code example: + * + *

+   * Ebean.startTransaction(); try { // do some fetching
+   * and or persisting
+   * 
+   * // commit at the end Ebean.commitTransaction();
+   * 
+   * } finally { // if commit didn't occur then rollback the transaction
+   * Ebean.endTransaction(); }
+   * 
+ * + *

+ * + * @see Ebean#endTransaction() + */ + public void endTransaction(); + + /** + * Refresh the values of a bean. + *

+ * Note that this does not refresh any OneToMany or ManyToMany properties. + *

+ * + * @see Ebean#refresh(Object) + */ + public void refresh(Object bean); + + /** + * Refresh a many property of an entity bean. + * + * @param bean + * the entity bean containing the 'many' property + * @param propertyName + * the 'many' property to be refreshed + * + * @see Ebean#refreshMany(Object, String) + */ + public void refreshMany(Object bean, String propertyName); + + /** + * Find a bean using its unique id. + * + * @see Ebean#find(Class, Object) + */ + public T find(Class beanType, Object uid); + + /** + * Get a reference Object (see {@link Ebean#getReference(Class, Object)}. + *

+ * This will not perform a query against the database. + *

+ * + * @see Ebean#getReference(Class, Object) + */ + public T getReference(Class beanType, Object uid); + + /** + * Return the number of 'top level' or 'root' entities this query should + * return. + */ + public int findRowCount(Query query, Transaction transaction); + + /** + * Return the Id values of the query as a List. + */ + public List findIds(Query query, Transaction t); + + /** + * Return a QueryIterator for the query. This is similar to findVisit in that + * not all the result beans need to be held in memory at the same time and as + * such is go for processing large queries. + */ + public QueryIterator findIterate(Query query, Transaction t); + + /** + * Execute the query visiting the results. This is similar to findIterate in + * that not all the result beans need to be held in memory at the same time + * and as such is go for processing large queries. + */ + public void findVisit(Query query, QueryResultVisitor visitor, Transaction t); + + /** + * Execute a query returning a list of beans. + *

+ * Generally you are able to use {@link Query#findList()} rather than + * explicitly calling this method. You could use this method if you wish to + * explicitly control the transaction used for the query. + *

+ * + * @param + * the type of entity bean to fetch. + * @param query + * the query to execute. + * @param transaction + * the transaction to use (can be null). + * @return the list of fetched beans. + * @see Query#findList() + */ + public List findList(Query query, Transaction transaction); + + /** + * Execute find row count query in a background thread. + *

+ * This returns a Future object which can be used to cancel, check the + * execution status (isDone etc) and get the value (with or without a + * timeout). + *

+ * + * @param query + * the query to execute the row count on + * @param t + * the transaction (can be null). + * @return a Future object for the row count query + */ + public FutureRowCount findFutureRowCount(Query query, Transaction t); + + /** + * Execute find Id's query in a background thread. + *

+ * This returns a Future object which can be used to cancel, check the + * execution status (isDone etc) and get the value (with or without a + * timeout). + *

+ * + * @param query + * the query to execute the fetch Id's on + * @param t + * the transaction (can be null). + * @return a Future object for the list of Id's + */ + public FutureIds findFutureIds(Query query, Transaction t); + + /** + * Execute find list query in a background thread. + *

+ * This returns a Future object which can be used to cancel, check the + * execution status (isDone etc) and get the value (with or without a + * timeout). + *

+ * + * @param query + * the query to execute in the background + * @param t + * the transaction (can be null). + * @return a Future object for the list result of the query + */ + public FutureList findFutureList(Query query, Transaction t); + + /** + * Execute find list SQL query in a background thread. + *

+ * This returns a Future object which can be used to cancel, check the + * execution status (isDone etc) and get the value (with or without a + * timeout). + *

+ * + * @param query + * the query to execute in the background + * @param t + * the transaction (can be null). + * @return a Future object for the list result of the query + */ + public SqlFutureList findFutureList(SqlQuery query, Transaction t); + + /** + * Find using a PagingList with explicit transaction and pageSize. + */ + public PagingList findPagingList(Query query, Transaction t, int pageSize); + + /** + * Execute the query returning a set of entity beans. + *

+ * Generally you are able to use {@link Query#findSet()} rather than + * explicitly calling this method. You could use this method if you wish to + * explicitly control the transaction used for the query. + *

+ * + * @param + * the type of entity bean to fetch. + * @param query + * the query to execute + * @param transaction + * the transaction to use (can be null). + * @return the set of fetched beans. + * @see Query#findSet() + */ + public Set findSet(Query query, Transaction transaction); + + /** + * Execute the query returning the entity beans in a Map. + *

+ * Generally you are able to use {@link Query#findMap()} rather than + * explicitly calling this method. You could use this method if you wish to + * explicitly control the transaction used for the query. + *

+ * + * @param + * the type of entity bean to fetch. + * @param query + * the query to execute. + * @param transaction + * the transaction to use (can be null). + * @return the map of fetched beans. + * @see Query#findMap() + */ + public Map findMap(Query query, Transaction transaction); + + /** + * Execute the query returning at most one entity bean. This will throw a + * PersistenceException if the query finds more than one result. + *

+ * Generally you are able to use {@link Query#findUnique()} rather than + * explicitly calling this method. You could use this method if you wish to + * explicitly control the transaction used for the query. + *

+ * + * @param + * the type of entity bean to fetch. + * @param query + * the query to execute. + * @param transaction + * the transaction to use (can be null). + * @return the list of fetched beans. + * @see Query#findUnique() + */ + public T findUnique(Query query, Transaction transaction); + + /** + * Execute the sql query returning a list of MapBean. + *

+ * Generally you are able to use {@link SqlQuery#findList()} rather than + * explicitly calling this method. You could use this method if you wish to + * explicitly control the transaction used for the query. + *

+ * + * @param query + * the query to execute. + * @param transaction + * the transaction to use (can be null). + * @return the list of fetched MapBean. + * @see SqlQuery#findList() + */ + public List findList(SqlQuery query, Transaction transaction); + + /** + * Execute the sql query returning a set of MapBean. + *

+ * Generally you are able to use {@link SqlQuery#findSet()} rather than + * explicitly calling this method. You could use this method if you wish to + * explicitly control the transaction used for the query. + *

+ * + * @param query + * the query to execute. + * @param transaction + * the transaction to use (can be null). + * @return the set of fetched MapBean. + * @see SqlQuery#findSet() + */ + public Set findSet(SqlQuery query, Transaction transaction); + + /** + * Execute the sql query returning a map of MapBean. + *

+ * Generally you are able to use {@link SqlQuery#findMap()} rather than + * explicitly calling this method. You could use this method if you wish to + * explicitly control the transaction used for the query. + *

+ * + * @param query + * the query to execute. + * @param transaction + * the transaction to use (can be null). + * @return the set of fetched MapBean. + * @see SqlQuery#findMap() + */ + public Map findMap(SqlQuery query, Transaction transaction); + + /** + * Execute the sql query returning a single MapBean or null. + *

+ * This will throw a PersistenceException if the query found more than one + * result. + *

+ *

+ * Generally you are able to use {@link SqlQuery#findUnique()} rather than + * explicitly calling this method. You could use this method if you wish to + * explicitly control the transaction used for the query. + *

+ * + * @param query + * the query to execute. + * @param transaction + * the transaction to use (can be null). + * @return the fetched MapBean or null if none was found. + * @see SqlQuery#findUnique() + */ + public SqlRow findUnique(SqlQuery query, Transaction transaction); + + /** + * Persist the bean by either performing an insert or update. + * + * @see Ebean#save(Object) + */ + public void save(Object bean) throws OptimisticLockException; + + /** + * Save all the beans in the iterator. + */ + public int save(Iterator it) throws OptimisticLockException; + + /** + * Save all the beans in the collection. + */ + public int save(Collection it) throws OptimisticLockException; + + /** + * Delete the bean. + * + * @see Ebean#delete(Object) + */ + public void delete(Object bean) throws OptimisticLockException; + + /** + * Delete all the beans from an Iterator. + */ + public int delete(Iterator it) throws OptimisticLockException; + + /** + * Delete all the beans in the collection. + */ + public int delete(Collection c) throws OptimisticLockException; + + /** + * Delete the bean given its type and id. + */ + public int delete(Class beanType, Object id); + + /** + * Delete the bean given its type and id with an explicit transaction. + */ + public int delete(Class beanType, Object id, Transaction t); + + /** + * Delete several beans given their type and id values. + */ + public void delete(Class beanType, Collection ids); + + /** + * Delete several beans given their type and id values with an explicit + * transaction. + */ + public void delete(Class beanType, Collection ids, Transaction t); + + /** + * Execute a SQL Update Delete or Insert statement using the current + * transaction. This returns the number of rows that where updated, deleted or + * inserted. + *

+ * Refer to Ebean.execute(UpdateSql) for full documentation. + *

+ * + * @see Ebean#execute(SqlUpdate) + */ + public int execute(SqlUpdate updSql); + + /** + * Execute a ORM insert update or delete statement using the current + * transaction. + *

+ * This returns the number of rows that where inserted, updated or deleted. + *

+ */ + public int execute(Update update); + + /** + * Execute a ORM insert update or delete statement with an explicit + * transaction. + */ + public int execute(Update update, Transaction t); + + /** + * Call a stored procedure. + *

+ * Refer to Ebean.execute(CallableSql) for full documentation. + *

+ * + * @see Ebean#execute(CallableSql) + */ + public int execute(CallableSql callableSql); + + /** + * Process committed changes from another framework. + *

+ * This notifies this instance of the framework that beans have been committed + * externally to it. Either by another framework or clustered server. It uses + * this to maintain its cache and text indexes appropriately. + *

+ * + * @see Ebean#externalModification(String, boolean, boolean, boolean) + */ + public void externalModification(String tableName, boolean inserted, boolean updated, + boolean deleted); + + /** + * Find a entity bean with an explicit transaction. + * + * @param + * the type of entity bean to find + * @param beanType + * the type of entity bean to find + * @param uid + * the bean id value + * @param transaction + * the transaction to use (can be null) + */ + public T find(Class beanType, Object uid, Transaction transaction); + + /** + * Insert or update a bean with an explicit transaction. + */ + public void save(Object bean, Transaction t) throws OptimisticLockException; + + /** + * Save all the beans in the iterator with an explicit transaction. + */ + public int save(Iterator it, Transaction t) throws OptimisticLockException; + + /** + * Force an update using the bean. + *

+ * You can use this method to FORCE an update to occur (even on a bean that + * has not been fetched but say built from JSON or XML). When + * {@link EbeanServer#save(Object)} is used Ebean determines whether to use an + * insert or an update based on the state of the bean. Using this method will + * force an update to occur. + *

+ *

+ * It is expected that this method is most useful in stateless REST services + * or web applications where you have the values you wish to update but no + * existing bean. + *

+ *

+ * For updates against beans that have not been fetched (say built from JSON + * or XML) this will treat deleteMissingChildren=true and will delete any + * 'missing children'. Refer to + * {@link EbeanServer#update(Object, Set, Transaction, boolean, boolean)}. + *

+ * + *
+   * 
+   * Customer c = new Customer();
+   * c.setId(7);
+   * c.setName("ModifiedNameNoOCC");
+   * 
+   * // generally you should set the version property
+   * // so that Optimistic Concurrency Checking is used.
+   * // If a version property is not set then no Optimistic
+   * // Concurrency Checking occurs for the update
+   * // c.setLastUpdate(lastUpdateTime);
+   * 
+   * // by default the Non-null properties
+   * // are included in the update
+   * ebeanServer.update(c);
+   * 
+   * 
+ */ + public void update(Object bean); + + /** + * Force an update of the non-null properties of the bean with an explicit + * transaction. + *

+ * You can use this method to FORCE an update to occur (even on a bean that + * has not been fetched but say built from JSON or XML). When + * {@link EbeanServer#save(Object)} is used Ebean determines whether to use an + * insert or an update based on the state of the bean. Using this method will + * force an update to occur. + *

+ *

+ * It is expected that this method is most useful in stateless REST services + * or web applications where you have the values you wish to update but no + * existing bean. + *

+ *

+ * For updates against beans that have not been fetched (say built from JSON + * or XML) this will treat deleteMissingChildren=true and will delete any + * 'missing children'. Refer to + * {@link EbeanServer#update(Object, Set, Transaction, boolean, boolean)}. + *

+ */ + public void update(Object bean, Transaction t); + + /** + * Force an update using the bean explicitly stating the properties to update. + *

+ * You can use this method to FORCE an update to occur (even on a bean that + * has not been fetched but say built from JSON or XML). When + * {@link EbeanServer#save(Object)} is used Ebean determines whether to use an + * insert or an update based on the state of the bean. Using this method will + * force an update to occur. + *

+ *

+ * It is expected that this method is most useful in stateless REST services + * or web applications where you have the values you wish to update but no + * existing bean. + *

+ *

+ * For updates against beans that have not been fetched (say built from JSON + * or XML) this will treat deleteMissingChildren=true and will delete any + * 'missing children'. Refer to + * {@link EbeanServer#update(Object, Set, Transaction, boolean, boolean)}. + *

+ * + *
+   * 
+   * Customer c = new Customer();
+   * c.setId(7);
+   * c.setName("ModifiedNameNoOCC");
+   * 
+   * // generally you should set the version property
+   * // so that Optimistic Concurrency Checking is used.
+   * // If a version property is not set then no Optimistic
+   * // Concurrency Checking occurs for the update
+   * // c.setLastUpdate(lastUpdateTime);
+   * 
+   * // by default the Non-null properties
+   * // are included in the update
+   * ebeanServer.update(c);
+   * 
+   * 
+ */ + public void update(Object bean, Set updateProps); + + /** + * Force an update of the specified properties of the bean with an explicit + * transaction. + *

+ * You can use this method to FORCE an update to occur (even on a bean that + * has not been fetched but say built from JSON or XML). When + * {@link EbeanServer#save(Object)} is used Ebean determines whether to use an + * insert or an update based on the state of the bean. Using this method will + * force an update to occur. + *

+ *

+ * It is expected that this method is most useful in stateless REST services + * or web applications where you have the values you wish to update but no + * existing bean. + *

+ *

+ * For updates against beans that have not been fetched (say built from JSON + * or XML) this will treat deleteMissingChildren=true and will delete any + * 'missing children'. Refer to + * {@link EbeanServer#update(Object, Set, Transaction, boolean, boolean)}. + *

+ */ + public void update(Object bean, Set updateProps, Transaction t); + + /** + * Force an update additionally specifying whether to 'deleteMissingChildren' + * when the update cascades to a OneToMany or ManyToMany. + *

+ * By default the deleteMissingChildren is true and it is assumed that when + * cascade saving a O2M or M2M relationship that the relationship is 'fully + * loaded' and any child beans that are no longer on the relationship will be + * deleted. + *

+ *

+ * You can use this method to FORCE an update to occur (even on a bean that + * has not been fetched but say built from JSON or XML). When + * {@link EbeanServer#save(Object)} is used Ebean determines whether to use an + * insert or an update based on the state of the bean. Using this method will + * force an update to occur. + *

+ *

+ * It is expected that this method is most useful in stateless REST services + * or web applications where you have the values you wish to update but no + * existing bean. + *

+ * + * @param bean + * the bean to update + * @param updateProps + * optionally you can specify the properties to update (can be null). + * @param t + * optionally you can specify the transaction to use (can be null). + * @param deleteMissingChildren + * specify false if you do not want 'missing children' of a OneToMany + * or ManyToMany to be automatically deleted. + * @param updateNullProperties + * specify true if by default you want properties with null values to + * be included in the update and false if those properties should be + * treated as 'unloaded' and excluded from the update. This only + * takes effect if the updateProps is null. + */ + public void update(Object bean, Set updateProps, Transaction t, + boolean deleteMissingChildren, boolean updateNullProperties); + + /** + * Force the bean to be saved with an explicit insert. + *

+ * Typically you would use save() and let Ebean determine if the bean should + * be inserted or updated. This can be useful when you are transferring data + * between databases and want to explicitly insert a bean into a different + * database that it came from. + *

+ */ + public void insert(Object bean); + + /** + * Force the bean to be saved with an explicit insert. + *

+ * Typically you would use save() and let Ebean determine if the bean should + * be inserted or updated. This can be useful when you are transferring data + * between databases and want to explicitly insert a bean into a different + * database that it came from. + *

+ */ + public void insert(Object bean, Transaction t); + + /** + * Delete the associations (from the intersection table) of a ManyToMany given + * the owner bean and the propertyName of the ManyToMany collection. + *

+ * Typically these deletions occur automatically when persisting a ManyToMany + * collection and this provides a way to invoke those deletions directly. + *

+ * + * @return the number of associations deleted (from the intersection table). + */ + public int deleteManyToManyAssociations(Object ownerBean, String propertyName); + + /** + * Delete the associations (from the intersection table) of a ManyToMany given + * the owner bean and the propertyName of the ManyToMany collection. + *

+ * Additionally specify a transaction to use. + *

+ *

+ * Typically these deletions occur automatically when persisting a ManyToMany + * collection and this provides a way to invoke those deletions directly. + *

+ * + * @return the number of associations deleted (from the intersection table). + */ + public int deleteManyToManyAssociations(Object ownerBean, String propertyName, Transaction t); + + /** + * Save the associations of a ManyToMany given the owner bean and the + * propertyName of the ManyToMany collection. + *

+ * Typically the saving of these associations (inserting into the intersection + * table) occurs automatically when persisting a ManyToMany. This provides a + * way to invoke those insertions directly. + *

+ */ + public void saveManyToManyAssociations(Object ownerBean, String propertyName); + + /** + * Save the associations of a ManyToMany given the owner bean and the + * propertyName of the ManyToMany collection. + *

+ * Typically the saving of these associations (inserting into the intersection + * table) occurs automatically when persisting a ManyToMany. This provides a + * way to invoke those insertions directly. + *

+ */ + public void saveManyToManyAssociations(Object ownerBean, String propertyName, Transaction t); + + /** + * Save the associated collection or bean given the property name. + *

+ * This is similar to performing a save cascade on a specific property + * manually. + *

+ *

+ * Note that you can turn on/off cascading for a transaction via + * {@link Transaction#setPersistCascade(boolean)} + *

+ * + * @param ownerBean + * the bean instance holding the property we want to save + * @param propertyName + * the property we want to save + */ + public void saveAssociation(Object ownerBean, String propertyName); + + /** + * Save the associated collection or bean given the property name with a + * specific transaction. + *

+ * This is similar to performing a save cascade on a specific property + * manually. + *

+ *

+ * Note that you can turn on/off cascading for a transaction via + * {@link Transaction#setPersistCascade(boolean)} + *

+ * + * @param ownerBean + * the bean instance holding the property we want to save + * @param propertyName + * the property we want to save + */ + public void saveAssociation(Object ownerBean, String propertyName, Transaction t); + + /** + * Delete the bean with an explicit transaction. + */ + public void delete(Object bean, Transaction t) throws OptimisticLockException; + + /** + * Delete all the beans from an iterator. + */ + public int delete(Iterator it, Transaction t) throws OptimisticLockException; + + /** + * Execute explicitly passing a transaction. + */ + public int execute(SqlUpdate updSql, Transaction t); + + /** + * Execute explicitly passing a transaction. + */ + public int execute(CallableSql callableSql, Transaction t); + + /** + * Execute a TxRunnable in a Transaction with an explicit scope. + *

+ * The scope can control the transaction type, isolation and rollback + * semantics. + *

+ */ + public void execute(TxScope scope, TxRunnable r); + + /** + * Execute a TxRunnable in a Transaction with the default scope. + *

+ * The default scope runs with REQUIRED and by default will rollback on any + * exception (checked or runtime). + *

+ */ + public void execute(TxRunnable r); + + /** + * Execute a TxCallable in a Transaction with an explicit scope. + *

+ * The scope can control the transaction type, isolation and rollback + * semantics. + *

+ */ + public T execute(TxScope scope, TxCallable c); + + /** + * Execute a TxCallable in a Transaction with the default scope. + *

+ * The default scope runs with REQUIRED and by default will rollback on any + * exception (checked or runtime). + *

+ */ + public T execute(TxCallable c); + + /** + * Return the manager of the server cache ("L2" cache). + * + */ + public ServerCacheManager getServerCacheManager(); + + /** + * Return the BackgroundExecutor service for asynchronous processing of + * queries. + */ + public BackgroundExecutor getBackgroundExecutor(); + + /** + * Run the cache warming queries on all bean types that have one defined. + *

+ * A cache warming query can be defined via {@link CacheStrategy}. + *

+ */ + public void runCacheWarming(); + + /** + * Run the cache warming query for a specific bean type. + *

+ * A cache warming query can be defined via {@link CacheStrategy}. + *

+ */ + public void runCacheWarming(Class beanType); + + /** + * Create a JsonContext that will use the default configuration options. + */ + public JsonContext createJsonContext(); + +} diff --git a/src/main/java/com/avaje/ebean/EbeanServerFactory.java b/src/main/java/com/avaje/ebean/EbeanServerFactory.java new file mode 100644 index 000000000..07003d545 --- /dev/null +++ b/src/main/java/com/avaje/ebean/EbeanServerFactory.java @@ -0,0 +1,98 @@ +package com.avaje.ebean; + +import javax.persistence.PersistenceException; + +import com.avaje.ebean.common.BootupEbeanManager; +import com.avaje.ebean.config.GlobalProperties; +import com.avaje.ebean.config.ServerConfig; +import com.avaje.ebean.util.ClassUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Creates EbeanServer instances. + *

+ * This uses either a ServerConfig or properties in the ebean.properties file to + * configure and create a EbeanServer instance. + *

+ *

+ * The EbeanServer instance can either be registered with the Ebean singleton or + * not. The Ebean singleton effectively holds a map of EbeanServers by a name. + * If the EbeanServer is registered with the Ebean singleton you can retrieve it + * later via {@link Ebean#getServer(String)}. + *

+ *

+ * One EbeanServer can be nominated as the 'default/primary' EbeanServer. Many + * methods on the Ebean singleton such as {@link Ebean#find(Class)} are just a + * convenient way of using the 'default/primary' EbeanServer. + *

+ * + * @author Rob Bygrave + * + */ +public class EbeanServerFactory { + + private static final Logger logger = LoggerFactory.getLogger(EbeanServerFactory.class); + + private static BootupEbeanManager serverFactory = createServerFactory(); + + /** + * Create using ebean.properties to configure the server. + */ + public static EbeanServer create(String name) { + + EbeanServer server = serverFactory.createServer(name); + + return server; + } + + /** + * Create using the ServerConfig object to configure the server. + */ + public static EbeanServer create(ServerConfig config) { + + if (config.getName() == null) { + throw new PersistenceException("The name is null (it is required)"); + } + + EbeanServer server = serverFactory.createServer(config); + + if (config.isDefaultServer()) { + GlobalProperties.setSkipPrimaryServer(true); + } + if (config.isRegister()) { + Ebean.register(server, config.isDefaultServer()); + } + + return server; + } + + private static BootupEbeanManager createServerFactory() { + + // String d___ = + // com.avaje.ebean.server.core.DefaultServerFactory.class.getName(); + String dflt = "com.avaje.ebeaninternal.server.core.DefaultServerFactory"; + String implClassName = GlobalProperties.get("ebean.serverfactory", dflt); + + int delaySecs = GlobalProperties.getInt("ebean.start.delay", 0); + if (delaySecs > 0) { + try { + // perhaps useful to delay the startup to give time to + // attach a debugger when running in a server like tomcat. + String m = "Ebean sleeping " + delaySecs + " seconds due to ebean.start.delay"; + logger.info(m); + Thread.sleep(delaySecs * 1000); + + } catch (InterruptedException e) { + String m = "Interrupting debug.start.delay of " + delaySecs; + logger.error(m, e); + } + } + try { + // use a client side implementation? + return (BootupEbeanManager) ClassUtil.newInstance(implClassName); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/src/main/java/com/avaje/ebean/ExampleExpression.java b/src/main/java/com/avaje/ebean/ExampleExpression.java new file mode 100644 index 000000000..fa335b980 --- /dev/null +++ b/src/main/java/com/avaje/ebean/ExampleExpression.java @@ -0,0 +1,93 @@ +package com.avaje.ebean; + +/** + * Query by Example expression. + *

+ * Pass in an example entity and for each non-null scalar properties an + * expression is added. + *

+ *

+ * By Default this case sensitive, will ignore numeric zero values and will use + * a Like for string values (you must put in your own wildcards). + *

+ *

+ * To get control over the options you can create an ExampleExpression and set + * those options such as case insensitive etc. + *

+ * + *
+ * // create an example bean and set the properties
+ * // with the query parameters you want
+ * Customer example = new Customer();
+ * example.setName("Rob%");
+ * example.setNotes("%something%");
+ * 
+ * List<Customer> list =
+ *     Ebean.find(Customer.class)
+ *         .where()
+ *         // pass the bean into the where() clause
+ *         .exampleLike(example)
+ *         // you can add other expressions to the same query
+ *         .gt("id", 2)
+ *         .findList();
+ * 
+ * 
+ * + * Similarly you can create an ExampleExpression + * + *
+ * Customer example = new Customer();
+ * example.setName("Rob%");
+ * example.setNotes("%something%");
+ * 
+ * // create a ExampleExpression with more control
+ * ExampleExpression qbe = new ExampleExpression(example, true, LikeType.EQUAL_TO)
+ *     .includeZeros();
+ * 
+ * List<Customer> list =
+ *     Ebean.find(Customer.class)
+ *         .where()
+ *         .add(qbe)
+ *         .findList();
+ * 
+ * + * @author Rob Bygrave + */ +public interface ExampleExpression extends Expression { + + /** + * By calling this method zero value properties are going to be included in + * the expression. + *

+ * By default numeric zero values are excluded as they can result from + * primitive int and long types. + *

+ */ + public ExampleExpression includeZeros(); + + /** + * Set case insensitive to true. + */ + public ExampleExpression caseInsensitive(); + + /** + * Use startsWith expression for string properties. + */ + public ExampleExpression useStartsWith(); + + /** + * Use contains expression for string properties. + */ + public ExampleExpression useContains(); + + /** + * Use endsWith expression for string properties. + */ + public ExampleExpression useEndsWith(); + + /** + * Use equal to expression for string properties. + */ + public ExampleExpression useEqualTo(); + +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/Expr.java b/src/main/java/com/avaje/ebean/Expr.java new file mode 100644 index 000000000..bc35fa530 --- /dev/null +++ b/src/main/java/com/avaje/ebean/Expr.java @@ -0,0 +1,314 @@ +package com.avaje.ebean; + +import java.util.Collection; +import java.util.Map; + +/** + * Expression factory for creating standard expressions for WHERE and HAVING + * clauses. + *

+ * Generally you will only need to use this object for creating OR, JUNCTION or + * CONJUNCTION expressions. To create simple expressions you will most likely + * just use the methods on the ExpressionList object that is returned via + * {@link Query#where()}. + *

+ *

+ * This provides a convenient way to create expressions for the 'Default' + * server. It is actually a short cut for using the ExpressionFactory of the + * 'default' EbeanServer. + *

+ * See also {@link Ebean#getExpressionFactory()} + *

+ *

+ * Creates standard common expressions for using in a Query Where or Having + * clause. + *

+ * + *
+ *  // Example: Using an Expr.or() method
+ * Query<Order> query = Ebean.createQuery(Order.class);
+ * query.where( 
+ * 		Expr.or(Expr.eq("status", Order.NEW),
+ *     		    Expr.gt("orderDate", lastWeek));
+ *     
+ * List<Order> list = query.findList();
+ * ...
+ * 
+ * + * @see Query#where() + * @author Rob Bygrave + */ +public class Expr { + + private Expr() { + } + + /** + * Equal To - property equal to the given value. + */ + public static Expression eq(String propertyName, Object value) { + return Ebean.getExpressionFactory().eq(propertyName, value); + } + + /** + * Not Equal To - property not equal to the given value. + */ + public static Expression ne(String propertyName, Object value) { + return Ebean.getExpressionFactory().ne(propertyName, value); + } + + /** + * Case Insensitive Equal To - property equal to the given value (typically + * using a lower() function to make it case insensitive). + */ + public static Expression ieq(String propertyName, String value) { + return Ebean.getExpressionFactory().ieq(propertyName, value); + } + + /** + * Between - property between the two given values. + */ + public static Expression between(String propertyName, Object value1, Object value2) { + + return Ebean.getExpressionFactory().between(propertyName, value1, value2); + } + + /** + * Greater Than - property greater than the given value. + */ + public static Expression gt(String propertyName, Object value) { + return Ebean.getExpressionFactory().gt(propertyName, value); + } + + /** + * Greater Than or Equal to - property greater than or equal to the given + * value. + */ + public static Expression ge(String propertyName, Object value) { + return Ebean.getExpressionFactory().ge(propertyName, value); + } + + /** + * Less Than - property less than the given value. + */ + public static Expression lt(String propertyName, Object value) { + return Ebean.getExpressionFactory().lt(propertyName, value); + } + + /** + * Less Than or Equal to - property less than or equal to the given value. + */ + public static Expression le(String propertyName, Object value) { + return Ebean.getExpressionFactory().le(propertyName, value); + } + + /** + * Is Null - property is null. + */ + public static Expression isNull(String propertyName) { + return Ebean.getExpressionFactory().isNull(propertyName); + } + + /** + * Is Not Null - property is not null. + */ + public static Expression isNotNull(String propertyName) { + return Ebean.getExpressionFactory().isNotNull(propertyName); + } + + /** + * Case insensitive {@link #exampleLike(Object)} + */ + public static ExampleExpression iexampleLike(Object example) { + return Ebean.getExpressionFactory().iexampleLike(example); + } + + /** + * Create the query by Example expression which is case sensitive and using + * LikeType.RAW (you need to add you own wildcards % and _). + */ + public static ExampleExpression exampleLike(Object example) { + return Ebean.getExpressionFactory().exampleLike(example); + } + + /** + * Create the query by Example expression specifying more options. + */ + public static ExampleExpression exampleLike(Object example, boolean caseInsensitive, + LikeType likeType) { + return Ebean.getExpressionFactory().exampleLike(example, caseInsensitive, likeType); + } + + /** + * Like - property like value where the value contains the SQL wild card + * characters % (percentage) and _ (underscore). + */ + public static Expression like(String propertyName, String value) { + return Ebean.getExpressionFactory().like(propertyName, value); + } + + /** + * Case insensitive Like - property like value where the value contains the + * SQL wild card characters % (percentage) and _ (underscore). Typically uses + * a lower() function to make the expression case insensitive. + */ + public static Expression ilike(String propertyName, String value) { + return Ebean.getExpressionFactory().ilike(propertyName, value); + } + + /** + * Starts With - property like value%. + */ + public static Expression startsWith(String propertyName, String value) { + return Ebean.getExpressionFactory().startsWith(propertyName, value); + } + + /** + * Case insensitive Starts With - property like value%. Typically uses a + * lower() function to make the expression case insensitive. + */ + public static Expression istartsWith(String propertyName, String value) { + return Ebean.getExpressionFactory().istartsWith(propertyName, value); + } + + /** + * Ends With - property like %value. + */ + public static Expression endsWith(String propertyName, String value) { + return Ebean.getExpressionFactory().endsWith(propertyName, value); + } + + /** + * Case insensitive Ends With - property like %value. Typically uses a lower() + * function to make the expression case insensitive. + */ + public static Expression iendsWith(String propertyName, String value) { + return Ebean.getExpressionFactory().iendsWith(propertyName, value); + } + + /** + * Contains - property like %value%. + */ + public static Expression contains(String propertyName, String value) { + return Ebean.getExpressionFactory().contains(propertyName, value); + } + + /** + * Case insensitive Contains - property like %value%. Typically uses a lower() + * function to make the expression case insensitive. + */ + public static Expression icontains(String propertyName, String value) { + return Ebean.getExpressionFactory().icontains(propertyName, value); + } + + /** + * In - property has a value in the array of values. + */ + public static Expression in(String propertyName, Object[] values) { + return Ebean.getExpressionFactory().in(propertyName, values); + } + + /** + * In - using a subQuery. + */ + public static Expression in(String propertyName, Query subQuery) { + return Ebean.getExpressionFactory().in(propertyName, subQuery); + } + + /** + * In - property has a value in the collection of values. + */ + public static Expression in(String propertyName, Collection values) { + return Ebean.getExpressionFactory().in(propertyName, values); + } + + /** + * Id Equal to - ID property is equal to the value. + */ + public static Expression idEq(Object value) { + return Ebean.getExpressionFactory().idEq(value); + } + + /** + * All Equal - Map containing property names and their values. + *

+ * Expression where all the property names in the map are equal to the + * corresponding value. + *

+ * + * @param propertyMap + * a map keyed by property names. + */ + public static Expression allEq(Map propertyMap) { + return Ebean.getExpressionFactory().allEq(propertyMap); + } + + /** + * Add raw expression with a single parameter. + *

+ * The raw expression should contain a single ? at the location of the + * parameter. + *

+ */ + public static Expression raw(String raw, Object value) { + return Ebean.getExpressionFactory().raw(raw, value); + } + + /** + * Add raw expression with an array of parameters. + *

+ * The raw expression should contain the same number of ? as there are + * parameters. + *

+ */ + public static Expression raw(String raw, Object[] values) { + return Ebean.getExpressionFactory().raw(raw, values); + } + + /** + * Add raw expression with no parameters. + */ + public static Expression raw(String raw) { + return Ebean.getExpressionFactory().raw(raw); + } + + /** + * And - join two expressions with a logical and. + */ + public static Expression and(Expression expOne, Expression expTwo) { + + return Ebean.getExpressionFactory().and(expOne, expTwo); + } + + /** + * Or - join two expressions with a logical or. + */ + public static Expression or(Expression expOne, Expression expTwo) { + + return Ebean.getExpressionFactory().or(expOne, expTwo); + } + + /** + * Negate the expression (prefix it with NOT). + */ + public static Expression not(Expression exp) { + + return Ebean.getExpressionFactory().not(exp); + } + + /** + * Return a list of expressions that will be joined by AND's. + */ + public static Junction conjunction(Query query) { + + return Ebean.getExpressionFactory().conjunction(query); + } + + /** + * Return a list of expressions that will be joined by OR's. + */ + public static Junction disjunction(Query query) { + + return Ebean.getExpressionFactory().disjunction(query); + } +} diff --git a/src/main/java/com/avaje/ebean/Expression.java b/src/main/java/com/avaje/ebean/Expression.java new file mode 100644 index 000000000..28308a1d2 --- /dev/null +++ b/src/main/java/com/avaje/ebean/Expression.java @@ -0,0 +1,10 @@ +package com.avaje.ebean; + +import java.io.Serializable; + +/** + * An expression that is part of a WHERE or HAVING clause. + */ +public interface Expression extends Serializable { + +} diff --git a/src/main/java/com/avaje/ebean/ExpressionFactory.java b/src/main/java/com/avaje/ebean/ExpressionFactory.java new file mode 100644 index 000000000..b9a59c8cc --- /dev/null +++ b/src/main/java/com/avaje/ebean/ExpressionFactory.java @@ -0,0 +1,257 @@ +package com.avaje.ebean; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Expression factory for creating standard expressions. + *

+ * Creates standard common expressions for using in a Query Where or Having + * clause. + *

+ *

+ * You will often not use this class directly but instead just add expressions + * via the methods on ExpressionList such as + * {@link ExpressionList#gt(String, Object)}. + *

+ *

+ * The ExpressionList is returned from {@link Query#where()}. + *

+ * + *
+ *  // Example: fetch orders where status equals new or orderDate > lastWeek.
+ *  
+ * Expression newOrLastWeek = 
+ *   Expr.or(Expr.eq("status", Order.Status.NEW), 
+ *           Expr.gt("orderDate", lastWeek));
+ * 
+ * Query<Order> query = Ebean.createQuery(Order.class);
+ * query.where().add(newOrLastWeek);
+ * List<Order> list = query.findList();
+ * ...
+ * 
+ * + * @see Query#where() + */ +public interface ExpressionFactory { + + /** + * Return the language for this expression factory. + */ + public String getLang(); + + /** + * Equal To - property equal to the given value. + */ + public Expression eq(String propertyName, Object value); + + /** + * Not Equal To - property not equal to the given value. + */ + public Expression ne(String propertyName, Object value); + + /** + * Case Insensitive Equal To - property equal to the given value (typically + * using a lower() function to make it case insensitive). + */ + public Expression ieq(String propertyName, String value); + + /** + * Between - property between the two given values. + */ + public Expression between(String propertyName, Object value1, Object value2); + + /** + * Between - value between two given properties. + */ + public Expression betweenProperties(String lowProperty, String highProperty, Object value); + + /** + * Greater Than - property greater than the given value. + */ + public Expression gt(String propertyName, Object value); + + /** + * Greater Than or Equal to - property greater than or equal to the given + * value. + */ + public Expression ge(String propertyName, Object value); + + /** + * Less Than - property less than the given value. + */ + public Expression lt(String propertyName, Object value); + + /** + * Less Than or Equal to - property less than or equal to the given value. + */ + public Expression le(String propertyName, Object value); + + /** + * Is Null - property is null. + */ + public Expression isNull(String propertyName); + + /** + * Is Not Null - property is not null. + */ + public Expression isNotNull(String propertyName); + + /** + * Case insensitive {@link #exampleLike(Object)} + */ + public ExampleExpression iexampleLike(Object example); + + /** + * Create the query by Example expression which is case sensitive and using + * LikeType.RAW (you need to add you own wildcards % and _). + */ + public ExampleExpression exampleLike(Object example); + + /** + * Create the query by Example expression specifying more options. + */ + public ExampleExpression exampleLike(Object example, boolean caseInsensitive, LikeType likeType); + + /** + * Like - property like value where the value contains the SQL wild card + * characters % (percentage) and _ (underscore). + */ + public Expression like(String propertyName, String value); + + /** + * Case insensitive Like - property like value where the value contains the + * SQL wild card characters % (percentage) and _ (underscore). Typically uses + * a lower() function to make the expression case insensitive. + */ + public Expression ilike(String propertyName, String value); + + /** + * Starts With - property like value%. + */ + public Expression startsWith(String propertyName, String value); + + /** + * Case insensitive Starts With - property like value%. Typically uses a + * lower() function to make the expression case insensitive. + */ + public Expression istartsWith(String propertyName, String value); + + /** + * Ends With - property like %value. + */ + public Expression endsWith(String propertyName, String value); + + /** + * Case insensitive Ends With - property like %value. Typically uses a lower() + * function to make the expression case insensitive. + */ + public Expression iendsWith(String propertyName, String value); + + /** + * Contains - property like %value%. + */ + public Expression contains(String propertyName, String value); + + /** + * Case insensitive Contains - property like %value%. Typically uses a lower() + * function to make the expression case insensitive. + */ + public Expression icontains(String propertyName, String value); + + /** + * In - property has a value in the array of values. + */ + public Expression in(String propertyName, Object[] values); + + /** + * In - using a subQuery. + */ + public Expression in(String propertyName, Query subQuery); + + /** + * In - property has a value in the collection of values. + */ + public Expression in(String propertyName, Collection values); + + /** + * Id Equal to - ID property is equal to the value. + */ + public Expression idEq(Object value); + + /** + * Id IN a list of Id values. + */ + public Expression idIn(List idList); + + /** + * All Equal - Map containing property names and their values. + *

+ * Expression where all the property names in the map are equal to the + * corresponding value. + *

+ * + * @param propertyMap + * a map keyed by property names. + */ + public Expression allEq(Map propertyMap); + + /** + * Add raw expression with a single parameter. + *

+ * The raw expression should contain a single ? at the location of the + * parameter. + *

+ */ + public Expression raw(String raw, Object value); + + /** + * Add raw expression with an array of parameters. + *

+ * The raw expression should contain the same number of ? as there are + * parameters. + *

+ */ + public Expression raw(String raw, Object[] values); + + /** + * Add raw expression with no parameters. + */ + public Expression raw(String raw); + + /** + * And - join two expressions with a logical and. + */ + public Expression and(Expression expOne, Expression expTwo); + + /** + * Or - join two expressions with a logical or. + */ + public Expression or(Expression expOne, Expression expTwo); + + /** + * Negate the expression (prefix it with NOT). + */ + public Expression not(Expression exp); + + /** + * Return a list of expressions that will be joined by AND's. + */ + public Junction conjunction(Query query); + + /** + * Return a list of expressions that will be joined by OR's. + */ + public Junction disjunction(Query query); + + /** + * Return a list of expressions that will be joined by AND's. + */ + public Junction conjunction(Query query, ExpressionList parent); + + /** + * Return a list of expressions that will be joined by OR's. + */ + public Junction disjunction(Query query, ExpressionList parent); +} diff --git a/src/main/java/com/avaje/ebean/ExpressionList.java b/src/main/java/com/avaje/ebean/ExpressionList.java new file mode 100644 index 000000000..571360ea5 --- /dev/null +++ b/src/main/java/com/avaje/ebean/ExpressionList.java @@ -0,0 +1,574 @@ +package com.avaje.ebean; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * List of Expressions that make up a where or having clause. + *

+ * An ExpressionList is returned from {@link Query#where()}. + *

+ *

+ * The ExpressionList has a list of convenience methods that create the standard + * expressions and add them to this list. + *

+ *

+ * The ExpressionList also duplicates methods that are found on the Query such + * as findList() and orderBy(). The purpose of these methods is provide a fluid + * API. The upside of this approach is that you can build and execute a query + * via chained methods. The down side is that this ExpressionList object has + * more methods than you would initially expect (the ones duplicated from + * Query). + *

+ * + * @see Query#where() + */ +public interface ExpressionList extends Serializable { + + /** + * Return the query that owns this expression list. + *

+ * This is a convenience method solely to support a fluid API where the + * methods are chained together. Adding expressions returns this expression + * list and this method can be used after that to return back the original + * query so that further things can be added to it. + *

+ */ + public Query query(); + + /** + * Set the order by clause replacing the existing order by clause if there is + * one. + *

+ * This follows SQL syntax using commas between each property with the + * optional asc and desc keywords representing ascending and descending order + * respectively. + *

+ *

+ * This is EXACTLY the same as {@link #orderBy(String)}. + *

+ */ + public Query order(String orderByClause); + + /** + * Return the OrderBy so that you can append an ascending or descending + * property to the order by clause. + *

+ * This will never return a null. If no order by clause exists then an 'empty' + * OrderBy object is returned. + *

+ */ + public OrderBy order(); + + /** + * Return the OrderBy so that you can append an ascending or descending + * property to the order by clause. + *

+ * This will never return a null. If no order by clause exists then an 'empty' + * OrderBy object is returned. + *

+ */ + public OrderBy orderBy(); + + /** + * Add an orderBy clause to the query. + * + * @see Query#orderBy(String) + */ + public Query orderBy(String orderBy); + + /** + * Add an orderBy clause to the query. + * + * @see Query#orderBy(String) + */ + public Query setOrderBy(String orderBy); + + /** + * Execute the query iterating over the results. + * + * @see Query#findIterate() + */ + public QueryIterator findIterate(); + + /** + * Execute the query visiting the results. + * + * @see Query#findVisit(QueryResultVisitor) + */ + public void findVisit(QueryResultVisitor visitor); + + /** + * Execute the query returning a list. + * + * @see Query#findList() + */ + public List findList(); + + /** + * Execute the query returning the list of Id's. + * + * @see Query#findIds() + */ + public List findIds(); + + /** + * Return the count of entities this query should return. + *

+ * This is the number of 'top level' or 'root level' entities. + *

+ */ + public int findRowCount(); + + /** + * Execute the query returning a set. + * + * @see Query#findSet() + */ + public Set findSet(); + + /** + * Execute the query returning a map. + * + * @see Query#findMap() + */ + public Map findMap(); + + /** + * Return a typed map specifying the key property and type. + */ + public Map findMap(String keyProperty, Class keyType); + + /** + * Execute the query returning a single bean. + * + * @see Query#findUnique() + */ + public T findUnique(); + + /** + * Execute find row count query in a background thread. + *

+ * This returns a Future object which can be used to cancel, check the + * execution status (isDone etc) and get the value (with or without a + * timeout). + *

+ * + * @return a Future object for the row count query + */ + public FutureRowCount findFutureRowCount(); + + /** + * Execute find Id's query in a background thread. + *

+ * This returns a Future object which can be used to cancel, check the + * execution status (isDone etc) and get the value (with or without a + * timeout). + *

+ * + * @return a Future object for the list of Id's + */ + public FutureIds findFutureIds(); + + /** + * Execute find list query in a background thread. + *

+ * This returns a Future object which can be used to cancel, check the + * execution status (isDone etc) and get the value (with or without a + * timeout). + *

+ * + * @return a Future object for the list result of the query + */ + public FutureList findFutureList(); + + /** + * Return a PagingList for this query. + *

+ * This can be used to break up a query into multiple queries to fetch the + * data a page at a time. + *

+ *

+ * This typically works by using a query per page and setting + * {@link Query#setFirstRow(int)} and and {@link Query#setMaxRows(int)} on the + * query. This usually would translate into SQL that uses limit offset, rownum + * or row_number function to limit the result set. + *

+ * + * @param pageSize + * the number of beans fetched per Page + * + */ + public PagingList findPagingList(int pageSize); + + public ExpressionList filterMany(String prop); + + /** + * Specify specific properties to fetch on the main/root bean (aka partial + * object). + * + * @see Query#select(String) + */ + public Query select(String properties); + + /** + * Specify a property (associated bean) to join and fetch including + * all its properties. + * + * @see Query#join(String) + */ + public Query join(String assocProperties); + + /** + * Specify a property (associated bean) to join and fetch with its + * specific properties to include (aka partial object). + * + * @see Query#join(String,String) + */ + public Query join(String assocProperty, String assocProperties); + + /** + * Set the first row to fetch. + * + * @see Query#setFirstRow(int) + */ + public Query setFirstRow(int firstRow); + + /** + * Set the maximum number of rows to fetch. + * + * @see Query#setMaxRows(int) + */ + public Query setMaxRows(int maxRows); + + /** + * Set the number of rows after which the fetching should continue in a + * background thread. + * + * @see Query#setBackgroundFetchAfter(int) + */ + public Query setBackgroundFetchAfter(int backgroundFetchAfter); + + /** + * Set the name of the property which values become the key of a map. + * + * @see Query#setMapKey(String) + */ + public Query setMapKey(String mapKey); + + /** + * Set a QueryListener for bean by bean processing. + * + * @see Query#setListener(QueryListener) + */ + public Query setListener(QueryListener queryListener); + + /** + * Set to true to use the query for executing this query. + * + * @see Query#setUseCache(boolean) + */ + public Query setUseCache(boolean useCache); + + /** + * Add expressions to the having clause. + *

+ * The having clause is only used for queries based on raw sql (via SqlSelect + * annotation etc). + *

+ */ + public ExpressionList having(); + + /** + * Add another expression to the where clause. + */ + public ExpressionList where(); + + /** + * Add an Expression to the list. + *

+ * This returns the list so that add() can be chained. + *

+ * + *
+   * Query<Customer> query = Ebean.createQuery(Customer.class);
+   * query.where()
+   *     .like("name","Rob%")
+   *     .eq("status", Customer.ACTIVE);
+   * List<Customer> list = query.findList();
+   * ...
+   * 
+ */ + public ExpressionList add(Expression expr); + + /** + * Add a list of Expressions to this ExpressionList.s + */ + public ExpressionList addAll(ExpressionList exprList); + + /** + * Equal To - property is equal to a given value. + */ + public ExpressionList eq(String propertyName, Object value); + + /** + * Not Equal To - property not equal to the given value. + */ + public ExpressionList ne(String propertyName, Object value); + + /** + * Case Insensitive Equal To - property equal to the given value (typically + * using a lower() function to make it case insensitive). + */ + public ExpressionList ieq(String propertyName, String value); + + /** + * Between - property between the two given values. + */ + public ExpressionList between(String propertyName, Object value1, Object value2); + + /** + * Between - value between the two properties. + */ + public ExpressionList betweenProperties(String lowProperty, String highProperty, Object value); + + /** + * Greater Than - property greater than the given value. + */ + public ExpressionList gt(String propertyName, Object value); + + /** + * Greater Than or Equal to - property greater than or equal to the given + * value. + */ + public ExpressionList ge(String propertyName, Object value); + + /** + * Less Than - property less than the given value. + */ + public ExpressionList lt(String propertyName, Object value); + + /** + * Less Than or Equal to - property less than or equal to the given value. + */ + public ExpressionList le(String propertyName, Object value); + + /** + * Is Null - property is null. + */ + public ExpressionList isNull(String propertyName); + + /** + * Is Not Null - property is not null. + */ + public ExpressionList isNotNull(String propertyName); + + /** + * A "Query By Example" type of expression. + *

+ * Pass in an example entity and for each non-null scalar properties an + * expression is added. + *

+ *

+ * By Default this case sensitive, will ignore numeric zero values and will + * use a Like for string values (you must put in your own wildcards). + *

+ *

+ * To get control over the options you can create an ExampleExpression and set + * those options such as case insensitive etc. + *

+ * + *
+   * // create an example bean and set the properties
+   * // with the query parameters you want
+   * Customer example = new Customer();
+   * example.setName("Rob%");
+   * example.setNotes("%something%");
+   * 
+   * List<Customer> list = Ebean.find(Customer.class).where()
+   *     // pass the bean into the where() clause
+   *     .exampleLike(example)
+   *     // you can add other expressions to the same query
+   *     .gt("id", 2).findList();
+   * 
+   * 
+ * + * Similarly you can create an ExampleExpression + * + *
+   * Customer example = new Customer();
+   * example.setName("Rob%");
+   * example.setNotes("%something%");
+   * 
+   * // create a ExampleExpression with more control
+   * ExampleExpression qbe = new ExampleExpression(example, true, LikeType.EQUAL_TO).includeZeros();
+   * 
+   * List<Customer> list = Ebean.find(Customer.class).where().add(qbe).findList();
+   * 
+ */ + public ExpressionList exampleLike(Object example); + + /** + * Case insensitive version of {@link #exampleLike(Object)} + */ + public ExpressionList iexampleLike(Object example); + + /** + * Like - property like value where the value contains the SQL wild card + * characters % (percentage) and _ (underscore). + */ + public ExpressionList like(String propertyName, String value); + + /** + * Case insensitive Like - property like value where the value contains the + * SQL wild card characters % (percentage) and _ (underscore). Typically uses + * a lower() function to make the expression case insensitive. + */ + public ExpressionList ilike(String propertyName, String value); + + /** + * Starts With - property like value%. + */ + public ExpressionList startsWith(String propertyName, String value); + + /** + * Case insensitive Starts With - property like value%. Typically uses a + * lower() function to make the expression case insensitive. + */ + public ExpressionList istartsWith(String propertyName, String value); + + /** + * Ends With - property like %value. + */ + public ExpressionList endsWith(String propertyName, String value); + + /** + * Case insensitive Ends With - property like %value. Typically uses a lower() + * function to make the expression case insensitive. + */ + public ExpressionList iendsWith(String propertyName, String value); + + /** + * Contains - property like %value%. + */ + public ExpressionList contains(String propertyName, String value); + + /** + * Case insensitive Contains - property like %value%. Typically uses a lower() + * function to make the expression case insensitive. + */ + public ExpressionList icontains(String propertyName, String value); + + /** + * In - using a subQuery. + */ + public ExpressionList in(String propertyName, Query subQuery); + + /** + * In - property has a value in the array of values. + */ + public ExpressionList in(String propertyName, Object... values); + + /** + * In - property has a value in the collection of values. + */ + public ExpressionList in(String propertyName, Collection values); + + /** + * Id IN a list of id values. + */ + public ExpressionList idIn(List idValues); + + /** + * Id Equal to - ID property is equal to the value. + */ + public ExpressionList idEq(Object value); + + /** + * All Equal - Map containing property names and their values. + *

+ * Expression where all the property names in the map are equal to the + * corresponding value. + *

+ * + * @param propertyMap + * a map keyed by property names. + */ + public ExpressionList allEq(Map propertyMap); + + /** + * Add raw expression with a single parameter. + *

+ * The raw expression should contain a single ? at the location of the + * parameter. + *

+ *

+ * When properties in the clause are fully qualified as table-column names + * then they are not translated. logical property name names (not fully + * qualified) will still be translated to their physical name. + *

+ */ + public ExpressionList raw(String raw, Object value); + + /** + * Add raw expression with an array of parameters. + *

+ * The raw expression should contain the same number of ? as there are + * parameters. + *

+ *

+ * When properties in the clause are fully qualified as table-column names + * then they are not translated. logical property name names (not fully + * qualified) will still be translated to their physical name. + *

+ */ + public ExpressionList raw(String raw, Object[] values); + + /** + * Add raw expression with no parameters. + *

+ * When properties in the clause are fully qualified as table-column names + * then they are not translated. logical property name names (not fully + * qualified) will still be translated to their physical name. + *

+ */ + public ExpressionList raw(String raw); + + /** + * And - join two expressions with a logical and. + */ + public ExpressionList and(Expression expOne, Expression expTwo); + + /** + * Or - join two expressions with a logical or. + */ + public ExpressionList or(Expression expOne, Expression expTwo); + + /** + * Negate the expression (prefix it with NOT). + */ + public ExpressionList not(Expression exp); + + /** + * Return a list of expressions that will be joined by AND's. + */ + public Junction conjunction(); + + /** + * Return a list of expressions that will be joined by OR's. + */ + public Junction disjunction(); + + /** + * End a Conjunction or Disjunction returning the parent expression list. + *

+ * Alternatively you can always use where() to return the top level expression + * list. + *

+ */ + public ExpressionList endJunction(); + +} diff --git a/src/main/java/com/avaje/ebean/FetchConfig.java b/src/main/java/com/avaje/ebean/FetchConfig.java new file mode 100644 index 000000000..a2ad44d30 --- /dev/null +++ b/src/main/java/com/avaje/ebean/FetchConfig.java @@ -0,0 +1,247 @@ +package com.avaje.ebean; + +import java.io.Serializable; + +/** + * Defines the configuration options for a "query fetch" or a + * "lazy loading fetch". This gives you the ability to use multiple smaller + * queries to populate an object graph as opposed to a single large query. + *

+ * The primary goal is to provide efficient ways of loading complex object + * graphs avoiding SQL Cartesian product and issues around populating object + * graphs that have multiple *ToMany relationships. + *

+ *

+ * It also provides the ability to control the lazy loading queries (batch size, + * selected properties and fetches) to avoid N+1 queries etc. + *

+ * There can also be cases loading across a single OneToMany where 2 SQL queries + * using Ebean FetchConfig.query() can be more efficient than one SQL query. + * When the "One" side is wide (lots of columns) and the cardinality difference + * is high (a lot of "Many" beans per "One" bean) then this can be more + * efficient loaded as 2 SQL queries. + *

+ * + *
+ * // Normal fetch join results in a single SQL query
+ * List<Order> list = Ebean.find(Order.class).join("details").findList();
+ * 
+ * // Find Orders join details using a single SQL query
+ * 
+ *

+ * Example: Using a "query join" instead of a "fetch join" we instead use 2 SQL + * queries + *

+ * + *
+ * // This will use 2 SQL queries to build this object graph
+ * List<Order> list =
+ *     Ebean.find(Order.class)
+ *         .fetch("details", new FetchConfig().query())
+ *         .findList();
+ * 
+ * // query 1) find order
+ * // query 2) find orderDetails where order.id in (?,?...) // first 100 order id's
+ * 
+ *

+ * Example: Using 2 "query joins" + *

+ * + *
+ * // This will use 3 SQL queries to build this object graph
+ * List<Order> list =
+ *     Ebean.find(Order.class)
+ *         .fetch("details", new JoinConfig().query())
+ *         .fetch("customer", new JoinConfig().query(5))
+ *         .findList();
+ * 
+ * // query 1) find order
+ * // query 2) find orderDetails where order.id in (?,?...) // first 100 order id's
+ * // query 3) find customer where id in (?,?,?,?,?) // first 5 customers
+ * 
+ *

+ * Example: Using "query joins" and partial objects + *

+ * + *
+ * // This will use 3 SQL queries to build this object graph
+ * List<Order> list =
+ *     Ebean.find(Order.class)
+ *         .select("status, shipDate")
+ *         .fetch("details", "quantity, price", new FetchConfig().query())
+ *         .fetch("details.product", "sku, name")
+ *         .fetch("customer", "name", new FetchConfig().query(10))
+ *         .fetch("customer.contacts")
+ *         .fetch("customer.shippingAddress")
+ *         .findList();
+ * 
+ * // query 1) find order (status, shipDate)
+ * // query 2) find orderDetail (quantity, price) fetch product (sku, name) where
+ * // order.id in (?,? ...)
+ * // query 3) find customer (name) fetch contacts (*) fetch shippingAddress (*)
+ * // where id in (?,?,?,?,?)
+ * 
+ * // Note: the fetch of "details.product" is automatically included into the
+ * // fetch of "details"
+ * //
+ * // Note: the fetch of "customer.contacts" and "customer.shippingAddress"
+ * // are automatically included in the fetch of "customer"
+ * 
+ *

+ * You can use query() and lazy together on a single join. The query is executed + * immediately and the lazy defines the batch size to use for further lazy + * loading (if lazy loading is invoked). + *

+ * + *
+ * List<Order> list =
+ *     Ebean.find(Order.class)
+ *         .fetch("customer", new FetchConfig().query(3).lazy(10))
+ *         .findList();
+ * 
+ * // query 1) find order
+ * // query 2) find customer where id in (?,?,?) // first 3 customers
+ * // .. then if lazy loading of customers is invoked
+ * // .. use a batch size of 10 to load the customers
+ * 
+ * 
+ * + *

+ * Example of controlling the lazy loading query: + *

+ *

+ * This gives us the ability to optimise the lazy loading query for a given use + * case. + *

+ * + *
+ * List<Order> list = Ebean.find(Order.class)
+ *   .fetch("customer","name", new FetchConfig().lazy(5))
+ *   .fetch("customer.contacts","contactName, phone, email")
+ *   .fetch("customer.shippingAddress")
+ *   .where().eq("status",Order.Status.NEW)
+ *   .findList();
+ * 
+ * // query 1) find order where status = Order.Status.NEW
+ * //  
+ * // .. if lazy loading of customers is invoked 
+ * // .. use a batch size of 5 to load the customers 
+ *  
+ *       find  customer (name) 
+ *       fetch contact (contactName, phone, email) 
+ *       fetch shippingAddress (*) 
+ *       where id in (?,?,?,?,?)
+ * 
+ * 
+ * + * @author mario + * @author rbygrave + */ +public class FetchConfig implements Serializable { + + private static final long serialVersionUID = 1L; + + private int lazyBatchSize = -1; + + private int queryBatchSize = -1; + + private boolean queryAll; + + /** + * Construct the fetch configuration object. + */ + public FetchConfig() { + } + + /** + * Specify that this path should be lazy loaded using the default batch load + * size. + */ + public FetchConfig lazy() { + this.lazyBatchSize = 0; + return this; + } + + /** + * Specify that this path should be lazy loaded with a specified batch size. + * + * @param lazyBatchSize + * the batch size for lazy loading + */ + public FetchConfig lazy(int lazyBatchSize) { + this.lazyBatchSize = lazyBatchSize; + return this; + } + + /** + * Specify that this path should be loaded as a separate query (rather than as + * part of the main query). + *

+ * This will use the default batch size for separate query which is 100. + *

+ */ + public FetchConfig query() { + this.queryBatchSize = 0; + this.queryAll = true; + return this; + } + + /** + * Specify that this path should be loaded as a separate query (rather than as + * part of the main query). + *

+ * The queryBatchSize is the number of parent id's that this separate query + * will load per batch. + *

+ *

+ * This will load all beans on this path eagerly. + *

+ * + * @param queryBatchSize + * the batch size used to load beans on this path + */ + public FetchConfig query(int queryBatchSize) { + this.queryBatchSize = queryBatchSize; + this.queryAll = true; + return this; + } + + /** + * Similar to {@link #query(int)} but only fetches the first batch. + *

+ * If there are more parent beans than the batch size then they will not be + * loaded eagerly but instead use lazy loading. + *

+ * + * @param queryBatchSize + * the number of parent beans this path is populated for + */ + public FetchConfig queryFirst(int queryBatchSize) { + this.queryBatchSize = queryBatchSize; + this.queryAll = false; + return this; + } + + /** + * Return the batch size for lazy loading. + */ + public int getLazyBatchSize() { + return lazyBatchSize; + } + + /** + * Return the batch size for separate query load. + */ + public int getQueryBatchSize() { + return queryBatchSize; + } + + /** + * Return true if the query fetch should fetch 'all' rather than just the + * 'first' batch. + */ + public boolean isQueryAll() { + return queryAll; + } + +} diff --git a/src/main/java/com/avaje/ebean/Filter.java b/src/main/java/com/avaje/ebean/Filter.java new file mode 100644 index 000000000..a01b85294 --- /dev/null +++ b/src/main/java/com/avaje/ebean/Filter.java @@ -0,0 +1,197 @@ +package com.avaje.ebean; + +import java.util.List; +import java.util.Set; + +/** + * Provides support for filtering and sorting lists of entities without going + * back to the database. + *

+ * That is, it uses local in-memory sorting and filtering of a list of entity + * beans. It is not used in a Database query or invoke a Database query. + *

+ *

+ * You can optionally specify a sortByClause and if so, the sort will always + * execute prior to the filter expressions. You can specify any number of filter + * expressions and they are effectively joined by logical "AND". + *

+ *

+ * The result of the filter method will leave the original list unmodified and + * return a new List instance. + *

+ * + *
+ * 
+ * // get a list of entities (query execution statistics in this case)
+ * 
+ * List<MetaQueryStatistic> list =
+ *     Ebean.find(MetaQueryStatistic.class).findList();
+ * 
+ * long nowMinus24Hrs = System.currentTimeMillis() - 24 * (1000 * 60 * 60);
+ * 
+ * // sort and filter the list returning a filtered list...
+ * 
+ * List<MetaQueryStatistic> filteredList =
+ *     Ebean.filter(MetaQueryStatistic.class)
+ *         .sort("avgTimeMicros desc")
+ *         .gt("executionCount", 0)
+ *         .gt("lastQueryTime", nowMinus24Hrs)
+ *         .eq("autofetchTuned", true)
+ *         .maxRows(10)
+ *         .filter(list);
+ * 
+ * 
+ *

+ * The propertyNames can traverse the object graph (e.g. customer.name) by using + * dot notation. If any point during the object graph traversal to get a + * property value is null then null is returned. + *

+ * + *
+ * // examples of property names that 
+ * // ... will traverse the object graph
+ * // ... where customer is a property of our bean
+ * 
+ * customer.name
+ * customer.shippingAddress.city
+ * 
+ * + *

+ * + *
+ * 
+ * // get a list of entities (query execution statistics)
+ * 
+ * List<Order> orders =
+ *     Ebean.find(Order.class).findList();
+ * 
+ * // Apply a filter...
+ * 
+ * List<Order> filteredOrders =
+ *     Ebean.filter(Order.class)
+ *         .startsWith("customer.name", "Rob")
+ *         .eq("customer.shippingAddress.city", "Auckland")
+ *         .filter(orders);
+ * 
+ * 
+ * + * @param + * the entity bean type + */ +public interface Filter { + + /** + * Specify a sortByClause. + *

+ * The sort (if specified) will always execute first followed by the filter + * expressions. + *

+ *

+ * Refer to {@link Ebean#sort(List, String)} for more detail. + *

+ */ + public Filter sort(String sortByClause); + + /** + * Specify the maximum number of rows/elements to return. + */ + public Filter maxRows(int maxRows); + + /** + * Equal To - property equal to the given value. + */ + public Filter eq(String prop, Object value); + + /** + * Not Equal To - property not equal to the given value. + */ + public Filter ne(String propertyName, Object value); + + /** + * Case Insensitive Equal To. + */ + public Filter ieq(String propertyName, String value); + + /** + * Between - property between the two given values. + */ + public Filter between(String propertyName, Object value1, Object value2); + + /** + * Greater Than - property greater than the given value. + */ + public Filter gt(String propertyName, Object value); + + /** + * Greater Than or Equal to - property greater than or equal to the given + * value. + */ + public Filter ge(String propertyName, Object value); + + /** + * Less Than - property less than the given value. + */ + public Filter lt(String propertyName, Object value); + + /** + * Less Than or Equal to - property less than or equal to the given value. + */ + public Filter le(String propertyName, Object value); + + /** + * Is Null - property is null. + */ + public Filter isNull(String propertyName); + + /** + * Is Not Null - property is not null. + */ + public Filter isNotNull(String propertyName); + + /** + * Starts With. + */ + public Filter startsWith(String propertyName, String value); + + /** + * Case insensitive Starts With. + */ + public Filter istartsWith(String propertyName, String value); + + /** + * Ends With. + */ + public Filter endsWith(String propertyName, String value); + + /** + * Case insensitive Ends With. + */ + public Filter iendsWith(String propertyName, String value); + + /** + * Contains - property contains the string "value". + */ + public Filter contains(String propertyName, String value); + + /** + * Case insensitive Contains. + */ + public Filter icontains(String propertyName, String value); + + /** + * In - property has a value contained in the set of values. + */ + public Filter in(String propertyName, Set values); + + /** + * Apply the filter to the list returning a new list of the matching elements + * in the sorted order. + *

+ * The sourceList will remain unmodified. + *

+ * + * @return Returns a new list with the sorting and filters applied. + */ + public List filter(List sourceList); + +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/FutureIds.java b/src/main/java/com/avaje/ebean/FutureIds.java new file mode 100644 index 000000000..9bd07c9a1 --- /dev/null +++ b/src/main/java/com/avaje/ebean/FutureIds.java @@ -0,0 +1,34 @@ +package com.avaje.ebean; + +import java.util.List; +import java.util.concurrent.Future; + +/** + * FutureIds represents the result of a background query execution for the Id's. + *

+ * It extends the java.util.concurrent.Future with the ability to get the Id's + * while the query is still executing in the background. + *

+ * + * @author rbygrave + */ +public interface FutureIds extends Future> { + + /** + * Returns the original query used to fetch the Id's. + */ + public Query getQuery(); + + /** + * Return the list of Id's which could be partially populated. + *

+ * That is the query getting the id's could still be running and adding id's + * to this list. + *

+ *

+ * To get the list of Id's ensuring the query has finished use the + * {@link Future#get()} method instead of this one. + *

+ */ + public List getPartialIds(); +} diff --git a/src/main/java/com/avaje/ebean/FutureList.java b/src/main/java/com/avaje/ebean/FutureList.java new file mode 100644 index 000000000..c0eeb3523 --- /dev/null +++ b/src/main/java/com/avaje/ebean/FutureList.java @@ -0,0 +1,53 @@ +package com.avaje.ebean; + +import java.util.List; +import java.util.concurrent.Future; + +/** + * FutureList represents the result of a background query execution that will + * return a list of entities. + *

+ * It extends the java.util.concurrent.Future with the ability to cancel the + * query, check if it is finished and get the resulting list waiting for the + * query to finish (ie. the standard features of java.util.concurrent.Future). + *

+ *

+ * A simple example: + *

+ * + *
+ *  // create a query to find all orders
+ * Query<Order> query = Ebean.find(Order.class);
+ * 
+ *  // execute the query in a background thread
+ *  // immediately returning the futureList
+ * FutureList<Order> futureList = query.findFutureList();
+ * 
+ *  // do something else ... 
+ * 
+ * if (!futureList.isDone()){
+ * 	// we can cancel the query execution. This will cancel
+ * // the underlying query if that is supported by the JDBC
+ * // driver and database
+ * 	futureList.cancel(true);
+ * }
+ * 
+ * 
+ * if (!futureList.isCancelled()){
+ * 	// wait for the query to finish and return the list
+ * 	List<Order> list = futureList.get();
+ * 	...
+ * }
+ * 
+ * 
+ * + * @author rbygrave + */ +public interface FutureList extends Future> { + + /** + * Return the query that is being executed by a background thread. + */ + public Query getQuery(); + +} diff --git a/src/main/java/com/avaje/ebean/FutureRowCount.java b/src/main/java/com/avaje/ebean/FutureRowCount.java new file mode 100644 index 000000000..b1fdeae7f --- /dev/null +++ b/src/main/java/com/avaje/ebean/FutureRowCount.java @@ -0,0 +1,15 @@ +package com.avaje.ebean; + +import java.util.concurrent.Future; + +/** + * Represents the result of a background query execution for the total row count + * for a query. + *

+ * It extends the java.util.concurrent.Future. + *

+ * + * @author rbygrave + */ +public interface FutureRowCount extends Future { +} diff --git a/src/main/java/com/avaje/ebean/Junction.java b/src/main/java/com/avaje/ebean/Junction.java new file mode 100644 index 000000000..ece8deeed --- /dev/null +++ b/src/main/java/com/avaje/ebean/Junction.java @@ -0,0 +1,78 @@ +package com.avaje.ebean; + +/** + * Represents a Conjunction or a Disjunction. + *

+ * Basically with a Conjunction you join together many expressions with AND, and + * with a Disjunction you join together many expressions with OR. + *

+ *

+ * Note: where() always takes you to the top level WHERE expression list. + *

+ * + *
+ * Query q =
+ *     Ebean.find(Person.class)
+ *         .where().disjunction()
+ *         .like("name", "Rob%")
+ *         .eq("status", Status.NEW)
+ * 
+ *         // where() returns us to the top level expression list
+ *         .where().gt("id", 10);
+ * 
+ * // read as...
+ * // where ( ((name like Rob%) or (status = NEW)) AND (id > 10) )
+ * 
+ * + *

+ * Note: endJunction() takes you to the parent expression list + *

+ * + *
+ * Query q =
+ *     Ebean.find(Person.class)
+ *         .where().disjunction()
+ *         .like("name", "Rob%")
+ *         .eq("status", Status.NEW)
+ *         .endJunction()
+ * 
+ *         // endJunction().. takes us to the 'parent' expression list
+ *         // which in this case is the top level (same as where())
+ * 
+ *         .gt("id", 10);
+ * 
+ * // read as...
+ * // where ( ((name like Rob%) or (status = NEW)) AND (id > 10) )
+ * 
+ * + *

+ * Example of a nested disjunction. + *

+ * + *
+ * Query<Customer> q = 
+ *  Ebean.find(Customer.class)
+ *      .where()
+ *          .disjunction()
+ *              .conjunction()
+ *                  .startsWith("name", "r")
+ *                  .eq("anniversary", onAfter)
+ *                  .endJunction()
+ *              .conjunction()
+ *                  .eq("status", Customer.Status.ACTIVE)
+ *                  .gt("id", 0)
+ *                  .endJunction()
+ *      .order().asc("name");
+ * 
+ * q.findList();
+ * String s = q.getGeneratedSql();
+ * 
+ *  // this produces an expression like:
+ *  
+ *  ( name like ? and c.anniversary = ? ) or (c.status = ?  and c.id > ? )
+ * 
+ * 
+ */ +public interface Junction extends Expression, ExpressionList { + +} diff --git a/src/main/java/com/avaje/ebean/LikeType.java b/src/main/java/com/avaje/ebean/LikeType.java new file mode 100644 index 000000000..b9588ab2c --- /dev/null +++ b/src/main/java/com/avaje/ebean/LikeType.java @@ -0,0 +1,35 @@ +package com.avaje.ebean; + +/** + * Used to specify the type of like matching used. + */ +public enum LikeType { + + /** + * You need to put in your own wildcards. + */ + RAW, + + /** + * The % wildcard is added to the end of the search word. + */ + STARTS_WITH, + + /** + * The % wildcard is added to the beginning of the search word. + */ + ENDS_WITH, + + /** + * The % wildcard is added to the beginning and end of the search word. + */ + CONTAINS, + + /** + * Uses equal to rather than a LIKE with wildcards. + *

+ * This is mainly here to be available for use with ExampleExpression. + *

+ */ + EQUAL_TO +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/LogLevel.java b/src/main/java/com/avaje/ebean/LogLevel.java new file mode 100644 index 000000000..d8d691368 --- /dev/null +++ b/src/main/java/com/avaje/ebean/LogLevel.java @@ -0,0 +1,25 @@ +package com.avaje.ebean; + +/** + * The transaction log level. + *

+ * This is used to define how much Ebean should log such as generated SQL. + *

+ */ +public enum LogLevel { + + /** + * No logging. + */ + NONE, + + /** + * Log only a summary level. + */ + SUMMARY, + + /** + * Log generated SQL/DML and binding variables. + */ + SQL +} diff --git a/src/main/java/com/avaje/ebean/OrderBy.java b/src/main/java/com/avaje/ebean/OrderBy.java new file mode 100644 index 000000000..38da01765 --- /dev/null +++ b/src/main/java/com/avaje/ebean/OrderBy.java @@ -0,0 +1,326 @@ +package com.avaje.ebean; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Represents an Order By for a Query. + *

+ * Is a ordered list of OrderBy.Property objects each specifying a property and + * whether it is ascending or descending order. + *

+ *

+ * Typically you will not construct an OrderBy yourself but use one that exists + * on the Query object. + *

+ * + * @author rbygrave + */ +public final class OrderBy implements Serializable { + + private static final long serialVersionUID = 9157089257745730539L; + + private transient Query query; + + private List list; + + /** + * Create an empty OrderBy with no associated query. + */ + public OrderBy() { + this.list = new ArrayList(2); + } + + /** + * Create an orderBy parsing the order by clause. + *

+ * The order by clause follows SQL order by clause with comma's between each + * property and optionally "asc" or "desc" to represent ascending or + * descending order respectively. + *

+ */ + public OrderBy(String orderByClause) { + this(null, orderByClause); + } + + /** + * Construct with a given query and order by clause. + */ + public OrderBy(Query query, String orderByClause) { + this.query = query; + this.list = new ArrayList(2); + parse(orderByClause); + } + + /** + * Reverse the ascending/descending order on all the properties. + */ + public void reverse() { + for (int i = 0; i < list.size(); i++) { + list.get(i).reverse(); + } + } + + /** + * Add a property with ascending order to this OrderBy. + */ + public Query asc(String propertyName) { + + list.add(new Property(propertyName, true)); + return query; + } + + /** + * Add a property with descending order to this OrderBy. + */ + public Query desc(String propertyName) { + + list.add(new Property(propertyName, false)); + return query; + } + + /** + * Return the properties for this OrderBy. + */ + public List getProperties() { + // not returning an Immutable list at this point + return list; + } + + /** + * Return true if this OrderBy does not have any properties. + */ + public boolean isEmpty() { + return list.isEmpty(); + } + + /** + * Return the associated query if there is one. + */ + public Query getQuery() { + return query; + } + + /** + * Associate this OrderBy with a query. + */ + public void setQuery(Query query) { + this.query = query; + } + + /** + * Return a copy of the OrderBy. + */ + public OrderBy copy() { + + OrderBy copy = new OrderBy(); + for (int i = 0; i < list.size(); i++) { + copy.add(list.get(i).copy()); + } + return copy; + } + + /** + * Add a property to the order by. + */ + public void add(Property p) { + list.add(p); + } + + public String toString() { + return list.toString(); + } + + /** + * Returns the OrderBy in string format. + */ + public String toStringFormat() { + if (list.isEmpty()) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < list.size(); i++) { + Property property = list.get(i); + if (i > 0) { + sb.append(", "); + } + sb.append(property.toStringFormat()); + } + return sb.toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof OrderBy) { + if (obj == this) { + return true; + } + OrderBy other = (OrderBy) obj; + return hashCode() == other.hashCode(); + } + return false; + } + + @Override + public int hashCode() { + return hash(); + } + + /** + * Return a hash value for this OrderBy. This can be to determine logical + * equality for OrderBy clauses. + */ + public int hash() { + int hc = OrderBy.class.getName().hashCode(); + for (int i = 0; i < list.size(); i++) { + hc = hc * 31 + list.get(i).hash(); + } + return hc; + } + + /** + * A property and its ascending descending order. + */ + public static final class Property implements Serializable { + + private static final long serialVersionUID = 1546009780322478077L; + + private String property; + + private boolean ascending; + + public Property(String property, boolean ascending) { + this.property = property; + this.ascending = ascending; + } + + protected int hash() { + int hc = property.hashCode(); + hc = hc * 31 + (ascending ? 0 : 1); + return hc; + } + + public String toString() { + return toStringFormat(); + } + + public String toStringFormat() { + if (ascending) { + return property; + } else { + return property + " desc"; + } + } + + /** + * Reverse the ascending/descending order for this property. + */ + public void reverse() { + this.ascending = !ascending; + } + + /** + * Trim off the pathPrefix. + */ + public void trim(String pathPrefix) { + property = property.substring(pathPrefix.length() + 1); + } + + /** + * Return a copy of this property. + */ + public Property copy() { + return new Property(property, ascending); + } + + /** + * Return the property name. + */ + public String getProperty() { + return property; + } + + /** + * Set the property name. + */ + public void setProperty(String property) { + this.property = property; + } + + /** + * Return true if the order is ascending. + */ + public boolean isAscending() { + return ascending; + } + + /** + * Set to true if the order is ascending. + */ + public void setAscending(boolean ascending) { + this.ascending = ascending; + } + + } + + private void parse(String orderByClause) { + + if (orderByClause == null) { + return; + } + + String[] chunks = orderByClause.split(","); + for (int i = 0; i < chunks.length; i++) { + + String[] pairs = chunks[i].split(" "); + Property p = parseProperty(pairs); + if (p != null) { + list.add(p); + } + } + } + + private Property parseProperty(String[] pairs) { + if (pairs.length == 0) { + return null; + } + + ArrayList wordList = new ArrayList(pairs.length); + for (int i = 0; i < pairs.length; i++) { + if (!isEmptyString(pairs[i])) { + wordList.add(pairs[i]); + } + } + if (wordList.isEmpty()) { + return null; + } + if (wordList.size() == 1) { + return new Property(wordList.get(0), true); + } + if (wordList.size() == 2) { + boolean asc = isAscending(wordList.get(1)); + return new Property(wordList.get(0), asc); + } + String m = "Expecting a max of 2 words in [" + Arrays.toString(pairs) + + "] but got " + wordList.size(); + throw new RuntimeException(m); + } + + private boolean isAscending(String s) { + s = s.toLowerCase(); + if (s.startsWith("asc")) { + return true; + } + if (s.startsWith("desc")) { + return false; + } + String m = "Expecting [" + s + "] to be asc or desc?"; + throw new RuntimeException(m); + } + + private boolean isEmptyString(String s) { + return s == null || s.length() == 0; + } +} diff --git a/src/main/java/com/avaje/ebean/Page.java b/src/main/java/com/avaje/ebean/Page.java new file mode 100644 index 000000000..626e29dd9 --- /dev/null +++ b/src/main/java/com/avaje/ebean/Page.java @@ -0,0 +1,74 @@ +package com.avaje.ebean; + +import java.util.List; + +/** + * Represents a Page of results that is part of a PagingList. + *

+ * Typically a Page represents the data that is shown to the user at a single + * time - and the user 'pages' through a large list. + *

+ * + * @author rbygrave + * + * @param + * the entity bean type + * + * @see Query#findPagingList(int) + * @see PagingList + */ +public interface Page { + + /** + * Return the list of entities for this page. + */ + public List getList(); + + /** + * Return the total row count for all pages. + */ + public int getTotalRowCount(); + + /** + * Return the total number of pages. + */ + public int getTotalPageCount(); + + /** + * Return the index position of this page. + */ + public int getPageIndex(); + + /** + * Return true if there is a next page. + */ + public boolean hasNext(); + + /** + * Return true if there is a previous page. + */ + public boolean hasPrev(); + + /** + * Return the next page. + */ + public Page next(); + + /** + * Return the previous page. + */ + public Page prev(); + + /** + * Helper method to return a "X to Y of Z" string for this page where X is the + * first row, Y the last row and Z the total row count. + * + * @param to + * String to put between the first and last row + * @param of + * String to put between the last row and the total row count + * + * @return String of the format XtoYofZ. + */ + public String getDisplayXtoYofZ(String to, String of); +} diff --git a/src/main/java/com/avaje/ebean/PagingList.java b/src/main/java/com/avaje/ebean/PagingList.java new file mode 100644 index 000000000..a2dc38f21 --- /dev/null +++ b/src/main/java/com/avaje/ebean/PagingList.java @@ -0,0 +1,127 @@ +package com.avaje.ebean; + +import java.util.List; +import java.util.concurrent.Future; + +/** + * Used to page through a query result rather than fetching all the results in a + * single query. + *

+ * Has the ability to use background threads to 'fetch ahead' the next page and + * get the total row count. + *

+ *

+ * If you are building a stateless web application and not keeping the + * PagingList over multiple requests then there is not much to be gained in + * using PagingList. Instead you can just use {@link Query#setFirstRow(int)} and + * {@link Query#setMaxRows(int)}. + *

+ * + *

+ * If you are using PagingList is a stateful web application where the + * PagingList is held over multiple requests then PagingList provides the extra + * benefits of + *

    + *
  • Fetch ahead - automatically fetching the next page via background query + * execution
  • + *
  • Automatic propagation of the persistence context
  • + *
+ *

+ *

+ * So with PagingList when you use Page 2 it can automatically fetch Page 3 data + * in the background (using a findFutureList() query). It also automatically + * propagates the persistence context so that all the queries executed by the + * PagingList all use the same persistence context. + *

+ * + *
+ * PagingList<TOne> pagingList =
+ *     Ebean.find(TOne.class)
+ *         .where().gt("name", "2")
+ *         .findPagingList(10);
+ * 
+ * // get the row count in the background...
+ * // ... otherwise it is fetched on demand
+ * // ... when getRowCount() or getPageCount()
+ * // ... is called
+ * pagingList.getFutureRowCount();
+ * 
+ * // get the first page
+ * Page<TOne> page = pagingList.getPage(0);
+ * 
+ * // get the beans from the page as a list
+ * List<TOne> list = page.getList();
+ * 
+ * + * @author rbygrave + * + * @param + * the entity bean type + */ +public interface PagingList { + + /** + * Refresh will clear all the pages and row count forcing them to be + * re-fetched when next required. + */ + public void refresh(); + + // public void fetchAll(); + // public String? getOrderBy(); + // public void setOrderBy(String?); + + /** + * By default fetchAhead is true so use this to turn off fetchAhead. + *

+ * Set this to false if you don't want to fetch ahead using background + * fetching. + *

+ * If set to true (or left as to default) then the next page is fetched in the + * background as soon as the list is accessed. + *

+ */ + public PagingList setFetchAhead(boolean fetchAhead); + + /** + * Return the Future for getting the total row count. + */ + public Future getFutureRowCount(); + + /** + * Return the data for all the pages in the form of a single List. + *

+ * Iterating through this list will automatically fire the paging queries as + * required. + *

+ */ + public List getAsList(); + + /** + * Return the page size. This is the number of rows per page. + */ + public int getPageSize(); + + /** + * Return the total row count. + *

+ * This gets the result from getFutureRowCount and will wait until that query + * has completed. + *

+ */ + public int getTotalRowCount(); + + /** + * Return the total page count. + *

+ * This is based on the total row count. This will wait until the row count + * has returned if it has not already. + *

+ */ + public int getTotalPageCount(); + + /** + * Return the page for a given page position (starting at 0). + */ + public Page getPage(int i); + +} diff --git a/src/main/java/com/avaje/ebean/Query.java b/src/main/java/com/avaje/ebean/Query.java new file mode 100644 index 000000000..3aa98653f --- /dev/null +++ b/src/main/java/com/avaje/ebean/Query.java @@ -0,0 +1,1102 @@ +package com.avaje.ebean; + +import com.avaje.ebean.config.ServerConfig; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Object relational query for finding a List, Set, Map or single entity bean. + *

+ * Example: Create the query using the API. + *

+ * + *
+ * List<Order> orderList = 
+ *   Ebean.find(Order.class)
+ *     .fetch("customer")
+ *     .fetch("details")
+ *     .where()
+ *       .like("customer.name","rob%")
+ *       .gt("orderDate",lastWeek)
+ *     .orderBy("customer.id, id desc")
+ *     .setMaxRows(50)
+ *     .findList();
+ *   
+ * ...
+ * 
+ * + *

+ * Example: The same query using the query language + *

+ * + *
+ * String oql = 
+ *   	"  find  order "
+ *   	+" fetch customer "
+ *   	+" fetch details "
+ *   	+" where customer.name like :custName and orderDate > :minOrderDate "
+ *   	+" order by customer.id, id desc "
+ *   	+" limit 50 ";
+ *   
+ * Query<Order> query = Ebean.createQuery(Order.class, oql);
+ * query.setParameter("custName", "Rob%");
+ * query.setParameter("minOrderDate", lastWeek);
+ *   
+ * List<Order> orderList = query.findList();
+ * ...
+ * 
+ * + *

+ * Example: Using a named query called "with.cust.and.details" + *

+ * + *
+ * Query<Order> query = Ebean.createNamedQuery(Order.class,"with.cust.and.details");
+ * query.setParameter("custName", "Rob%");
+ * query.setParameter("minOrderDate", lastWeek);
+ *   
+ * List<Order> orderList = query.findList();
+ * ...
+ * 
+ * + *

Autofetch

+ *

+ * Ebean has built in support for "Autofetch". This is a mechanism where a query + * can be automatically tuned based on profiling information that is collected. + *

+ *

+ * This is effectively the same as automatically using select() and fetch() to + * build a query that will fetch all the data required by the application and no + * more. + *

+ *

+ * It is expected that Autofetch will be the default approach for many queries + * in a system. It is possibly not as useful where the result of a query is sent + * to a remote client or where there is some requirement for "Read Consistency" + * guarantees. + *

+ * + *

Query Language

+ *

+ * Partial Objects + *

+ *

+ * The find and fetch clauses support specifying a list of + * properties to fetch. This results in objects that are "partially populated". + * If you try to get a property that was not populated a "lazy loading" query + * will automatically fire and load the rest of the properties of the bean (This + * is very similar behaviour as a reference object being "lazy loaded"). + *

+ *

+ * Partial objects can be saved just like fully populated objects. If you do + * this you should remember to include the "Version" property in the + * initial fetch. If you do not include a version property then optimistic + * concurrency checking will occur but only include the fetched properties. + * Refer to "ALL Properties/Columns" mode of Optimistic Concurrency checking. + *

+ * + *
+ * [ find  {bean type} [ ( * | {fetch properties} ) ] ]
+ * [ fetch {associated bean} [ ( * | {fetch properties} ) ] ]
+ * [ where {predicates} ]
+ * [ order by {order by properties} ]
+ * [ limit {max rows} [ offset {first row} ] ]
+ * 
+ * + *

+ * FIND {bean type} [ ( * | {fetch properties} ) ] + *

+ *

+ * With the find you specify the type of beans to fetch. You can optionally + * specify a list of properties to fetch. If you do not specify a list of + * properties ALL the properties for those beans are fetched. + *

+ *

+ * In object graph terms the find clause specifies the type of bean at + * the root level and the fetch clauses specify the paths of the object + * graph to populate. + *

+ *

+ * FETCH {associated property} [ ( * | {fetch + * properties} ) ] + *

+ *

+ * With the fetch you specify the associated property to fetch and populate. The + * associated property is a OneToOnem, ManyToOne, OneToMany or ManyToMany + * property. When the query is executed Ebean will fetch the associated data. + *

+ *

+ * For fetch of a path we can optionally specify a list of properties to fetch. + * If you do not specify a list of properties ALL the properties for that bean + * type are fetched. + *

+ *

+ * WHERE {list of predicates} + *

+ *

+ * The list of predicates which are joined by AND OR NOT ( and ). They can + * include named (or positioned) bind parameters. These parameters will need to + * be bound by {@link Query#setParameter(String, Object)}. + *

+ *

+ * ORDER BY {order by properties} + *

+ *

+ * The list of properties to order the result. You can include ASC (ascending) + * and DESC (descending) in the order by clause. + *

+ *

+ * LIMIT {max rows} [ OFFSET {first row} ] + *

+ *

+ * The limit offset specifies the max rows and first row to fetch. The offset is + * optional. + *

+ *

Examples of Ebean's Query Language

+ *

+ * Find orders fetching all its properties + *

+ * + *
+ * find order
+ * 
+ * + *

+ * Find orders fetching all its properties + *

+ * + *
+ * find order (*)
+ * 
+ * + *

+ * Find orders fetching its id, shipDate and status properties. Note that the id + * property is always fetched even if it is not included in the list of fetch + * properties. + *

+ * + *
+ * find order (shipDate, status)
+ * 
+ * + *

+ * Find orders with a named bind variable (that will need to be bound via + * {@link Query#setParameter(String, Object)}). + *

+ * + *
+ * find order
+ * where customer.name like :custLike
+ * 
+ * + *

+ * Find orders and also fetch the customer with a named bind parameter. This + * will fetch and populate both the order and customer objects. + *

+ * + *
+ * find  order
+ * fetch customer
+ * where customer.id = :custId
+ * 
+ * + *

+ * Find orders and also fetch the customer, customer shippingAddress, order + * details and related product. Note that customer and product objects will be + * "Partial Objects" with only some of their properties populated. The customer + * objects will have their id, name and shipping address populated. The product + * objects (associated with each order detail) will have their id, sku and name + * populated. + *

+ * + *
+ * find  order
+ * fetch customer (name)
+ * fetch customer.shippingAddress
+ * fetch details
+ * fetch details.product (sku, name)
+ * 
+ * + *

Early parsing of the Query

+ *

+ * When you get a Query object from a named query, the query statement has + * already been parsed. You can then add to that query (add fetch paths, add to + * the where clause) or override some of its settings (override the order by + * clause, first rows, max rows). + *

+ *

+ * The thought is that you can use named queries as a 'starting point' and then + * modify the query to suit specific needs. + *

+ *

Building the Where clause

+ *

+ * You can add to the where clause using Expression objects or a simple String. + * Note that the ExpressionList has methods to add most of the common + * expressions that you will need. + *

    + *
  • where(String addToWhereClause)
  • + *
  • where().add(Expression expression)
  • + *
  • where().eq(propertyName, value).like(propertyName , value)...
  • + *
+ *

+ *

+ * The full WHERE clause is constructed by appending together + *

  • original query where clause (Named query or query.setQuery(String oql))
  • + *
  • clauses added via query.where(String addToWhereClause)
  • + *
  • clauses added by Expression objects
  • + *

    + *

    + * The above is the order that these are clauses are appended to give the full + * WHERE clause. + *

    + *

    Design Goal

    + *

    + * This query language is NOT designed to be a replacement for SQL. It is + * designed to be a simple way to describe the "Object Graph" you want Ebean to + * build for you. Each find/fetch represents a node in that "Object Graph" which + * makes it easy to define for each node which properties you want to fetch. + *

    + *

    + * Once you hit the limits of this language such as wanting aggregate functions + * (sum, average, min etc) or recursive queries etc you use SQL. Ebean's goal is + * to make it as easy as possible to use your own SQL to populate entity beans. + * Refer to {@link RawSql} . + *

    + * + * @param + * the type of Entity bean this query will fetch. + */ +public interface Query extends Serializable { + + /** + * How this query should use (or not) the Lucene Index if one is defined for + * the bean type. + */ + public enum UseIndex { + NO, DEFAULT, YES_IDS, YES_OBJECTS + } + + /** + * Explicitly specify how this query should use a Lucene Index if one is + * defined for this bean type. + */ + public Query setUseIndex(UseIndex useIndex); + + /** + * Return the setting for how this query should use a Lucene Index if one is + * defined for this bean type. + */ + public UseIndex getUseIndex(); + + /** + * Return the RawSql that was set to use for this query. + */ + public RawSql getRawSql(); + + /** + * Set RawSql to use for this query. + */ + public Query setRawSql(RawSql rawSql); + + /** + * Cancel the query execution if supported by the underlying database and + * driver. + *

    + * This must be called from a different thread to the query executor. + *

    + */ + public void cancel(); + + /** + * Return a copy of the query. + *

    + * This is so that you can use a Query as a "prototype" for creating other + * query instances. You could create a Query with various where expressions + * and use that as a "prototype" - using this copy() method to create a new + * instance that you can then add other expressions then execute. + *

    + */ + public Query copy(); + + /** + * Return the ExpressionFactory used by this query. + */ + public ExpressionFactory getExpressionFactory(); + + /** + * Returns true if this query was tuned by autoFetch. + */ + public boolean isAutofetchTuned(); + + /** + * Explicitly specify whether to use Autofetch for this query. + *

    + * If you do not call this method on a query the "Implicit Autofetch mode" is + * used to determine if Autofetch should be used for a given query. + *

    + *

    + * Autofetch can add additional fetch paths to the query and specify which + * properties are included for each path. If you have explicitly defined some + * fetch paths Autofetch will not remove. + *

    + */ + public Query setAutofetch(boolean autofetch); + + /** + * Explicitly set a comma delimited list of the properties to fetch on the + * 'main' entity bean (aka partial object). Note that '*' means all + * properties. + * + *
    +   * Query<Customer> query = Ebean.createQuery(Customer.class);
    +   * 
    +   * // Only fetch the customer id, name and status.
    +   * // This is described as a "Partial Object"
    +   * query.select("name, status");
    +   * query.where("lower(name) like :custname").setParameter("custname", "rob%");
    +   * 
    +   * List<Customer> customerList = query.findList();
    +   * 
    + * + * @param fetchProperties + * the properties to fetch for this bean (* = all properties). + */ + public Query select(String fetchProperties); + + /** + * Specify a path to fetch with its specific properties to include + * (aka partial object). + *

    + * When you specify a join this means that property (associated bean(s)) will + * be fetched and populated. If you specify "*" then all the properties of the + * associated bean will be fetched and populated. You can specify a comma + * delimited list of the properties of that associated bean which means that + * only those properties are fetched and populated resulting in a + * "Partial Object" - a bean that only has some of its properties populated. + *

    + * + *
    +   * // query orders...
    +   * Query<Order> query = Ebean.createQuery(Order.class);
    +   * 
    +   * // fetch the customer...
    +   * // ... getting the customer's name and phone number
    +   * query.fetch("customer", "name, phNumber");
    +   * 
    +   * // ... also fetch the customers billing address (* = all properties)
    +   * query.fetch("customer.billingAddress", "*");
    +   * 
    + * + *

    + * If columns is null or "*" then all columns/properties for that path are + * fetched. + *

    + * + *
    +   * // fetch customers (their id, name and status)
    +   * Query<Customer> query = Ebean.createQuery(Customer.class);
    +   * 
    +   * // only fetch some of the properties of the customers
    +   * query.select("name, status");
    +   * List<Customer> list = query.findList();
    +   * 
    + * + * @param path + * the path of an associated (1-1,1-M,M-1,M-M) bean. + * @param fetchProperties + * properties of the associated bean that you want to include in the + * fetch (* means all properties, null also means all properties). + */ + public Query fetch(String path, String fetchProperties); + + /** + * Additionally specify a FetchConfig to use a separate query or lazy loading + * to load this path. + */ + public Query fetch(String assocProperty, String fetchProperties, FetchConfig fetchConfig); + + /** + * Specify a path to load including all its properties. + *

    + * The same as {@link #fetch(String, String)} with the fetchProperties as "*". + *

    + * + * @param path + * the property of an associated (1-1,1-M,M-1,M-M) bean. + */ + public Query fetch(String path); + + /** + * Additionally specify a JoinConfig to specify a "query join" and or define + * the lazy loading query. + */ + public Query fetch(String path, FetchConfig joinConfig); + + /** + * Execute the query returning the list of Id's. + *

    + * This query will execute against the EbeanServer that was used to create it. + *

    + * + * @see EbeanServer#findIds(Query, Transaction) + */ + public List findIds(); + + /** + * Execute the query iterating over the results. + *

    + * Remember that with {@link QueryIterator} you must call + * {@link QueryIterator#close()} when you have finished iterating the results + * (typically in a finally block). + *

    + *

    + * This query will execute against the EbeanServer that was used to create it. + *

    + */ + public QueryIterator findIterate(); + + /** + * Execute the query using callbacks to a visitor to process the resulting + * beans one at a time. + *

    + * Similar to findIterate() this query method does not require all the result + * beans to be all held in memory at once and as such is useful for processing + * large queries. + *

    + * + *
    +   * 
    +   * Query<Customer> query = server.find(Customer.class)
    +   *     .fetch("contacts", new FetchConfig().query(2))
    +   *     .where().gt("id", 0)
    +   *     .orderBy("id")
    +   *     .setMaxRows(2);
    +   * 
    +   * query.findVisit(new QueryResultVisitor<Customer>() {
    +   * 
    +   *   public boolean accept(Customer customer) {
    +   *     // do something with customer
    +   *     System.out.println("-- visit " + customer);
    +   *     return true;
    +   *   }
    +   * });
    +   * 
    + * + * @param visitor + * the visitor used to process the queried beans. + */ + public void findVisit(QueryResultVisitor visitor); + + /** + * Execute the query returning the list of objects. + *

    + * This query will execute against the EbeanServer that was used to create it. + *

    + * + * @see EbeanServer#findList(Query, Transaction) + */ + public List findList(); + + /** + * Execute the query returning the set of objects. + *

    + * This query will execute against the EbeanServer that was used to create it. + *

    + * + * @see EbeanServer#findSet(Query, Transaction) + */ + public Set findSet(); + + /** + * Execute the query returning a map of the objects. + *

    + * This query will execute against the EbeanServer that was used to create it. + *

    + *

    + * You can use setMapKey() so specify the property values to be used as keys + * on the map. If one is not specified then the id property is used. + *

    + * + *
    +   * Query<Product> query = Ebean.createQuery(Product.class);
    +   * query.setMapKey("sku");
    +   * Map<?, Product> map = query.findMap();
    +   * 
    + * + * @see EbeanServer#findMap(Query, Transaction) + */ + public Map findMap(); + + /** + * Return a typed map specifying the key property and type. + */ + public Map findMap(String keyProperty, Class keyType); + + /** + * Execute the query returning either a single bean or null (if no matching + * bean is found). + *

    + * If more than 1 row is found for this query then a PersistenceException is + * thrown. + *

    + *

    + * This is useful when your predicates dictate that your query should only + * return 0 or 1 results. + *

    + * + *
    +   * // assuming the sku of products is unique...
    +   * Product product =
    +   *     Ebean.find(Product.class)
    +   *         .where("sku = ?")
    +   *         .set(1, "aa113")
    +   *         .findUnique();
    +   * ...
    +   * 
    + * + *

    + * It is also useful with finding objects by their id when you want to specify + * further join information. + *

    + * + *
    +   * // Fetch order 1 and additionally fetch join its order details...
    +   * Order order = 
    +   *     Ebean.find(Order.class)
    +   *       .setId(1)
    +   *       .fetch("details")
    +   *       .findUnique();
    +   *       
    +   * List<OrderDetail> details = order.getDetails();
    +   * ...
    +   * 
    + */ + public T findUnique(); + + /** + * Return the count of entities this query should return. + *

    + * This is the number of 'top level' or 'root level' entities. + *

    + */ + public int findRowCount(); + + /** + * Execute find row count query in a background thread. + *

    + * This returns a Future object which can be used to cancel, check the + * execution status (isDone etc) and get the value (with or without a + * timeout). + *

    + * + * @return a Future object for the row count query + */ + public FutureRowCount findFutureRowCount(); + + /** + * Execute find Id's query in a background thread. + *

    + * This returns a Future object which can be used to cancel, check the + * execution status (isDone etc) and get the value (with or without a + * timeout). + *

    + * + * @return a Future object for the list of Id's + */ + public FutureIds findFutureIds(); + + /** + * Execute find list query in a background thread. + *

    + * This returns a Future object which can be used to cancel, check the + * execution status (isDone etc) and get the value (with or without a + * timeout). + *

    + * + * @return a Future object for the list result of the query + */ + public FutureList findFutureList(); + + /** + * Return a PagingList for this query. + *

    + * This can be used to break up a query into multiple queries to fetch the + * data a page at a time. + *

    + *

    + * This typically works by using a query per page and setting + * {@link Query#setFirstRow(int)} and and {@link Query#setMaxRows(int)} on the + * query. This usually would translate into SQL that uses limit offset, rownum + * or row_number function to limit the result set. + *

    + * + * @param pageSize + * the number of beans fetched per Page + * + */ + public PagingList findPagingList(int pageSize); + + /** + * Set a named bind parameter. Named parameters have a colon to prefix the + * name. + * + *
    +   * // a query with a named parameter
    +   * String oql = "find order where status = :orderStatus";
    +   * 
    +   * Query<Order> query = Ebean.createQuery(Order.class, oql);
    +   * 
    +   * // bind the named parameter
    +   * query.bind("orderStatus", OrderStatus.NEW);
    +   * List<Order> list = query.findList();
    +   * 
    + * + * @param name + * the parameter name + * @param value + * the parameter value + */ + public Query setParameter(String name, Object value); + + /** + * Set an ordered bind parameter according to its position. Note that the + * position starts at 1 to be consistent with JDBC PreparedStatement. You need + * to set a parameter value for each ? you have in the query. + * + *
    +   * // a query with a positioned parameter
    +   * String oql = "where status = ? order by id desc";
    +   * 
    +   * Query<Order> query = Ebean.createQuery(Order.class, oql);
    +   * 
    +   * // bind the parameter
    +   * query.setParameter(1, OrderStatus.NEW);
    +   * 
    +   * List<Order> list = query.findList();
    +   * 
    + * + * @param position + * the parameter bind position starting from 1 (not 0) + * @param value + * the parameter bind value. + */ + public Query setParameter(int position, Object value); + + /** + * Set a listener to process the query on a row by row basis. + *

    + * Use this when you want to process a large query and do not want to hold the + * entire query result in memory. + *

    + *

    + * It this case the rows are not loaded into the persistence context and + * instead are processed by the query listener. + *

    + * + *
    +   * QueryListener<Order> listener = ...;
    +   *   
    +   * Query<Order> query  = Ebean.createQuery(Order.class);
    +   *   
    +   * // set the listener that will process each order one at a time
    +   * query.setListener(listener);
    +   *   
    +   * // execute the query. Note that the returned
    +   * // list (emptyList) will be empty ...
    +   * List<Order> emtyList = query.findList();
    +   * 
    + */ + public Query setListener(QueryListener queryListener); + + /** + * Set the Id value to query. This is used with findUnique(). + *

    + * You can use this to have further control over the query. For example adding + * fetch joins. + *

    + * + *
    +   * Query<Order> query = Ebean.createQuery(Order.class);
    +   * Order order = query.setId(1).join("details").findUnique();
    +   * List<OrderDetail> details = order.getDetails();
    +   * ...
    +   * 
    + */ + public Query setId(Object id); + + /** + * Add additional clause(s) to the where clause. + *

    + * This typically contains named parameters which will need to be set via + * {@link #setParameter(String, Object)}. + *

    + * + *
    +   * Query<Order> query = Ebean.createQuery(Order.class, "top");
    +   * ...
    +   * if (...) {
    +   *   query.where("status = :status and lower(customer.name) like :custName");
    +   *   query.setParameter("status", Order.NEW);
    +   *   query.setParameter("custName", "rob%");
    +   * }
    +   * 
    + * + *

    + * Internally the addToWhereClause string is processed by removing named + * parameters (replacing them with ?) and by converting logical property names + * to database column names (with table alias). The rest of the string is left + * as is and it is completely acceptable and expected for the addToWhereClause + * string to include sql functions and columns. + *

    + * + * @param addToWhereClause + * the clause to append to the where clause which typically contains + * named parameters. + * @return The query object + */ + public Query where(String addToWhereClause); + + /** + * Add a single Expression to the where clause returning the query. + * + *
    +   * List<Order> newOrders = 
    +   *     Ebean.find(Order.class)
    +   * 		.where().eq("status", Order.NEW)
    +   * 		.findList();
    +   * ...
    +   * 
    + */ + public Query where(Expression expression); + + /** + * Add Expressions to the where clause with the ability to chain on the + * ExpressionList. You can use this for adding multiple expressions to the + * where clause. + * + *
    +   * Query<Order> query = Ebean.createQuery(Order.class, "top");
    +   * ...
    +   * if (...) {
    +   *   query.where()
    +   *     .eq("status", Order.NEW)
    +   *     .ilike("customer.name","rob%");
    +   * }
    +   * 
    + * + * @see Expr + * @return The ExpressionList for adding expressions to. + */ + public ExpressionList where(); + + /** + * This applies a filter on the 'many' property list rather than the root + * level objects. + *

    + * Typically you will use this in a scenario where the cardinality is high on + * the 'many' property you wish to join to. Say you want to fetch customers + * and their associated orders... but instead of getting all the orders for + * each customer you only want to get the new orders they placed since last + * week. In this case you can use filterMany() to filter the orders. + *

    + * + *
    +   * 
    +   * List<Customer> list = Ebean
    +   *     .find(Customer.class)
    +   *     // .fetch("orders", new FetchConfig().lazy())
    +   *     // .fetch("orders", new FetchConfig().query())
    +   *     .fetch("orders").where().ilike("name", "rob%").filterMany("orders")
    +   *     .eq("status", Order.Status.NEW).gt(
    +   *         "orderDate", lastWeek).findList();
    +   * 
    +   * 
    + * + *

    + * Please note you have to be careful that you add expressions to the correct + * expression list - as there is one for the 'root level' and one for each + * filterMany that you have. + *

    + * + * @param propertyName + * the name of the many property that you want to have a filter on. + * + * @return the expression list that you add filter expressions for the many + * to. + */ + public ExpressionList filterMany(String propertyName); + + /** + * Add Expressions to the Having clause return the ExpressionList. + *

    + * Currently only beans based on raw sql will use the having clause. + *

    + *

    + * Note that this returns the ExpressionList (so you can add multiple + * expressions to the query in a fluent API way). + *

    + * + * @see Expr + * @return The ExpressionList for adding more expressions to. + */ + public ExpressionList having(); + + /** + * Add additional clause(s) to the having clause. + *

    + * This typically contains named parameters which will need to be set via + * {@link #setParameter(String, Object)}. + *

    + * + *
    +   * Query<ReportOrder> query = Ebean.createQuery(ReportOrder.class);
    +   * ...
    +   * if (...) {
    +   *   query.having("score > :min");
    +   *   query.setParameter("min", 1);
    +   * }
    +   * 
    + * + * @param addToHavingClause + * the clause to append to the having clause which typically contains + * named parameters. + * @return The query object + */ + public Query having(String addToHavingClause); + + /** + * Add an expression to the having clause returning the query. + *

    + * Currently only beans based on raw sql will use the having clause. + *

    + *

    + * This is similar to {@link #having()} except it returns the query rather + * than the ExpressionList. This is useful when you want to further specify + * something on the query. + *

    + * + * @param addExpressionToHaving + * the expression to add to the having clause. + * @return the Query object + */ + public Query having(Expression addExpressionToHaving); + + /** + * Set the order by clause replacing the existing order by clause if there is + * one. + *

    + * This follows SQL syntax using commas between each property with the + * optional asc and desc keywords representing ascending and descending order + * respectively. + *

    + *

    + * This is EXACTLY the same as {@link #order(String)}. + *

    + */ + public Query orderBy(String orderByClause); + + /** + * Set the order by clause replacing the existing order by clause if there is + * one. + *

    + * This follows SQL syntax using commas between each property with the + * optional asc and desc keywords representing ascending and descending order + * respectively. + *

    + *

    + * This is EXACTLY the same as {@link #orderBy(String)}. + *

    + */ + public Query order(String orderByClause); + + /** + * Return the OrderBy so that you can append an ascending or descending + * property to the order by clause. + *

    + * This will never return a null. If no order by clause exists then an 'empty' + * OrderBy object is returned. + *

    + *

    + * This is EXACTLY the same as {@link #orderBy()}. + *

    + */ + public OrderBy order(); + + /** + * Return the OrderBy so that you can append an ascending or descending + * property to the order by clause. + *

    + * This will never return a null. If no order by clause exists then an 'empty' + * OrderBy object is returned. + *

    + *

    + * This is EXACTLY the same as {@link #order()}. + *

    + */ + public OrderBy orderBy(); + + /** + * Set an OrderBy object to replace any existing OrderBy clause. + *

    + * This is EXACTLY the same as {@link #setOrderBy(OrderBy)}. + *

    + */ + public Query setOrder(OrderBy orderBy); + + /** + * Set an OrderBy object to replace any existing OrderBy clause. + *

    + * This is EXACTLY the same as {@link #setOrder(OrderBy)}. + *

    + */ + public Query setOrderBy(OrderBy orderBy); + + /** + * Set whether this query uses DISTINCT. + */ + public Query setDistinct(boolean isDistinct); + + /** + * Set this to true and the beans and collections returned will be plain + * classes rather than Ebean generated dynamic subclasses etc. + *

    + * This is *ONLY* relevant when you are not using enhancement (and using + * dynamic subclasses instead). + *

    + *

    + * Alternatively you can globally set the mode using ebean.vanillaMode=true in + * ebean.properties or {@link ServerConfig#setVanillaMode(boolean)}. + *

    + * + * @see ServerConfig#setVanillaMode(boolean) + * @see ServerConfig#setVanillaRefMode(boolean) + */ + public Query setVanillaMode(boolean vanillaMode); + + /** + * Return the first row value. + */ + public int getFirstRow(); + + /** + * Set the first row to return for this query. + * + * @param firstRow + */ + public Query setFirstRow(int firstRow); + + /** + * Return the max rows for this query. + */ + public int getMaxRows(); + + /** + * Set the maximum number of rows to return in the query. + * + * @param maxRows + * the maximum number of rows to return in the query. + */ + public Query setMaxRows(int maxRows); + + /** + * Set the rows after which fetching should continue in a background thread. + * + * @param backgroundFetchAfter + */ + public Query setBackgroundFetchAfter(int backgroundFetchAfter); + + /** + * Set the property to use as keys for a map. + *

    + * If no property is set then the id property is used. + *

    + * + *
    +   * // Assuming sku is unique for products...
    +   *    
    +   * Query<Product> query = Ebean.createQuery(Product.class);
    +   *   
    +   * // use sku for keys...
    +   * query.setMapKey("sku");
    +   *   
    +   * Map<?,Product> productMap = query.findMap();
    +   * ...
    +   * 
    + * + * @param mapKey + * the property to use as keys for a map. + */ + public Query setMapKey(String mapKey); + + /** + * Set this to true to use the bean cache. + *

    + * If the query result is in cache then by default this same instance is + * returned. In this sense it should be treated as a read only object graph. + *

    + */ + public Query setUseCache(boolean useBeanCache); + + /** + * Set this to true to use the query cache. + */ + public Query setUseQueryCache(boolean useQueryCache); + + /** + * When set to true when you want the returned beans to be read only. + */ + public Query setReadOnly(boolean readOnly); + + /** + * When set to true all the beans from this query are loaded into the bean + * cache. + */ + public Query setLoadBeanCache(boolean loadBeanCache); + + /** + * Set a timeout on this query. + *

    + * This will typically result in a call to setQueryTimeout() on a + * preparedStatement. If the timeout occurs an exception will be thrown - this + * will be a SQLException wrapped up in a PersistenceException. + *

    + * + * @param secs + * the query timeout limit in seconds. Zero means there is no limit. + */ + public Query setTimeout(int secs); + + /** + * A hint which for JDBC translates to the Statement.fetchSize(). + *

    + * Gives the JDBC driver a hint as to the number of rows that should be + * fetched from the database when more rows are needed for ResultSet. + *

    + */ + public Query setBufferFetchSizeHint(int fetchSize); + + /** + * Return the sql that was generated for executing this query. + *

    + * This is only available after the query has been executed and provided only + * for informational purposes. + *

    + */ + public String getGeneratedSql(); + + /** + * Return the total hits matched for a lucene text search query. + */ + public int getTotalHits(); + + /** + * executed the select with "for update" which should lock the record + * "on read" + */ + public Query setForUpdate(boolean forUpdate); + + public boolean isForUpdate(); +} diff --git a/src/main/java/com/avaje/ebean/QueryIterator.java b/src/main/java/com/avaje/ebean/QueryIterator.java new file mode 100644 index 000000000..f3754fa93 --- /dev/null +++ b/src/main/java/com/avaje/ebean/QueryIterator.java @@ -0,0 +1,60 @@ +package com.avaje.ebean; + +import java.util.Iterator; + +/** + * Used to provide iteration over query results. + *

    + * This can be used when you want to process a very large number of results and + * means that you don't have to hold all the results in memory at once (unlike + * findList(), findSet() etc where all the beans are held in the List or Set + * etc). + *

    + * + *
    + * 
    + * Query<Customer> query = server.find(Customer.class)
    + *     .fetch("contacts", new FetchConfig().query(2))
    + *     .where().gt("id", 0)
    + *     .orderBy("id")
    + *     .setMaxRows(2);
    + * 
    + * QueryIterator<Customer> it = query.findIterate();
    + * try {
    + *   while (it.hasNext()) {
    + *     Customer customer = it.next();
    + *     // do something with customer...
    + *   }
    + * } finally {
    + *   // close the associated resources
    + *   it.close();
    + * }
    + * 
    + * + * @author rbygrave + * + * @param + * the type of entity bean in the iteration + */ +public interface QueryIterator extends Iterator, java.io.Closeable { + + /** + * Returns true if the iteration has more elements. + */ + public boolean hasNext(); + + /** + * Returns the next element in the iteration. + */ + public T next(); + + /** + * Remove is not allowed. + */ + public void remove(); + + /** + * Close the underlying resources held by this iterator. + */ + public void close(); +} diff --git a/src/main/java/com/avaje/ebean/QueryListener.java b/src/main/java/com/avaje/ebean/QueryListener.java new file mode 100644 index 000000000..b3c6c9795 --- /dev/null +++ b/src/main/java/com/avaje/ebean/QueryListener.java @@ -0,0 +1,38 @@ +package com.avaje.ebean; + +/** + * Provides a mechanism for processing a query one bean at a time. + *

    + * This is useful when the query will return a large number of results and you + * want to process the beans one at a time rather than whole all of the beans in + * memory at once. + *

    + * + *
    + * QueryListener<Order> listener = ...;
    + *    
    + * Query<Order> query  = Ebean.createQuery(Order.class);
    + *    
    + * // set the listener that will process each order one at a time
    + * query.setListener(listener);
    + *    
    + * // execute the query. Note that the returned
    + * // list will be empty ... so don't bother assigning it
    + * query.findList();
    + * 
    + * + * @param + * the type of entity bean + */ +public interface QueryListener { + + /** + * Process the bean that has just been read. + *

    + * This bean will not be added to the List Set or Map and nor will it be put + * into the PersistenceContext. This is what makes this a good way to process + * a large result set (which could normally use a lot of memory). + *

    + */ + public void process(T bean); +} diff --git a/src/main/java/com/avaje/ebean/QueryResultVisitor.java b/src/main/java/com/avaje/ebean/QueryResultVisitor.java new file mode 100644 index 000000000..586622a84 --- /dev/null +++ b/src/main/java/com/avaje/ebean/QueryResultVisitor.java @@ -0,0 +1,49 @@ +package com.avaje.ebean; + +/** + * Used to process a query result one bean at a time via a callback to this + * visitor. + *

    + * If you wish to stop further processing return false from the accept method. + *

    + *

    + * Unlike findList() and findSet() using a QueryResultVisitor does not require + * all the beans in the query result to be held in memory at once. This makes + * QueryResultVisitor useful for processing large queries. + *

    + * + *
    + * 
    + * Query<Customer> query = server.find(Customer.class)
    + *     .fetch("contacts", new FetchConfig().query(2))
    + *     .where().gt("id", 0)
    + *     .orderBy("id")
    + *     .setMaxRows(2);
    + * 
    + * query.findVisit(new QueryResultVisitor<Customer>() {
    + * 
    + *   public boolean accept(Customer customer) {
    + *     // do something with customer
    + *     System.out.println("-- visit " + customer);
    + *     return true;
    + *   }
    + * });
    + * 
    + * + * @author rbygrave + * + * @param + * the type of entity bean being queried. + */ +public interface QueryResultVisitor { + + /** + * Process the bean and return true if you want to continue processing more + * beans. Return false if you want to stop processing further. + * + * @param bean + * the entity bean to process + * @return true to continue processing or false to stop. + */ + public boolean accept(T bean); +} diff --git a/src/main/java/com/avaje/ebean/RawSql.java b/src/main/java/com/avaje/ebean/RawSql.java new file mode 100644 index 000000000..ffc703aec --- /dev/null +++ b/src/main/java/com/avaje/ebean/RawSql.java @@ -0,0 +1,573 @@ +package com.avaje.ebean; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.avaje.ebean.util.CamelCaseHelper; + +/** + * Used to build object graphs based on a raw SQL statement (rather than + * generated by Ebean). + *

    + * If you don't want to build object graphs you can use {@link SqlQuery} instead + * which returns {@link SqlRow} objects rather than entity beans. + *

    + *

    + * Unparsed RawSql: + *

    + *

    + * When RawSql is created via RawSqlBuilder.unparsed(sql) then Ebean can not + * modify the SQL at all. It can't add any extra expressions into the SQL. + *

    + *

    + * Parsed RawSql: + *

    + *

    + * When RawSql is created via RawSqlBuilder.parse(sql) then Ebean will parse the + * SQL and find places in the SQL where it can add extra where expressions, add + * extra having expressions or replace the order by clause. If you want to + * explicitly tell Ebean where these insertion points are you can place special + * strings into your SQL (${where} or ${andWhere} and ${having} or + * ${andHaving}). + *

    + *

    + * If the SQL already includes a WHERE clause put in ${andWhere} in the location + * you want Ebean to add any extra where expressions. If the SQL doesn't have a + * WHERE clause put ${where} in instead. Similarly you can put in ${having} or + * ${andHaving} where you want Ebean put add extra having expressions. + *

    + *

    + * Aggregates: + *

    + *

    + * Often RawSql will be used with Aggregate functions (sum, avg, max etc). The + * follow example shows an example based on Total Order Amount - + * sum(d.order_qty*d.unit_price). + *

    + *

    + * We can use a OrderAggregate bean that has a @Sql to indicate it is based + * on RawSql and not based on a real DB Table or DB View. It has some properties + * to hold the values for the aggregate functions (sum etc) and a @OneToOne + * to Order. + *

    + *

    + *   + *

    + *

    + * Example OrderAggregate + *

    + * + *
    + *  ...
    + *  // @Sql indicates to that this bean
    + *  // is based on RawSql rather than a table
    + * 
    + * @Entity
    + * @Sql    
    + * public class OrderAggregate {
    + * 
    + *  @OneToOne
    + *  Order order;
    + *      
    + *  Double totalAmount;
    + *  
    + *  Double totalItems;
    + *  
    + *  // getters and setters
    + *  ...
    + * 
    + *

    + * Example 1: + *

    + * + *
    + * String sql = " select order_id, o.status, c.id, c.name, sum(d.order_qty*d.unit_price) as totalAmount"
    + *     + " from o_order o"
    + *     + " join o_customer c on c.id = o.kcustomer_id "
    + *     + " join o_order_detail d on d.order_id = o.id " + " group by order_id, o.status ";
    + * 
    + * RawSql rawSql = RawSqlBuilder.parse(sql)
    + *     // map the sql result columns to bean properties
    + *     .columnMapping("order_id", "order.id").columnMapping("o.status", "order.status")
    + *     .columnMapping("c.id", "order.customer.id")
    + *     .columnMapping("c.name", "order.customer.name")
    + *     // we don't need to map this one due to the sql column alias
    + *     // .columnMapping("sum(d.order_qty*d.unit_price)", "totalAmount")
    + *     .create();
    + * 
    + * Query<OrderAggregate> query = Ebean.find(OrderAggregate.class);
    + * query.setRawSql(rawSql).where().gt("order.id", 0).having().gt("totalAmount", 20);
    + * 
    + * List<OrderAggregate> list = query.findList();
    + * 
    + * + *

    + * Example 2: + *

    + * + *

    + * The following example uses a FetchConfig().query() so that after the initial + * RawSql query is executed Ebean executes a secondary query to fetch the + * associated order status, orderDate along with the customer name. + *

    + * + *
    + * String sql = " select order_id, 'ignoreMe', sum(d.order_qty*d.unit_price) as totalAmount "
    + *     + " from o_order_detail d"
    + *     + " group by order_id ";
    + * 
    + * RawSql rawSql = RawSqlBuilder.parse(sql).columnMapping("order_id", "order.id")
    + *     .columnMappingIgnore("'ignoreMe'").create();
    + * 
    + * Query<OrderAggregate> query = Ebean.find(OrderAggregate.class);
    + * query.setRawSql(rawSql).fetch("order", "status,orderDate", new FetchConfig().query())
    + *     .fetch("order.customer", "name").where()
    + *     .gt("order.id", 0).having().gt("totalAmount", 20).order().desc("totalAmount").setMaxRows(10);
    + * 
    + * 
    + * + *

    + * Note that lazy loading also works with object graphs built with RawSql. + *

    + * + * @author rbygrave + * + */ +public final class RawSql implements Serializable { + + private static final long serialVersionUID = 1L; + + private final Sql sql; + + private final ColumnMapping columnMapping; + + protected RawSql(Sql sql, ColumnMapping columnMapping) { + this.sql = sql; + this.columnMapping = columnMapping; + } + + /** + * Return the Sql either unparsed or in parsed (broken up) form. + */ + public Sql getSql() { + return sql; + } + + /** + * Return the column mapping for the SQL columns to bean properties. + */ + public ColumnMapping getColumnMapping() { + return columnMapping; + } + + /** + * Return the hash for this query. + */ + public int queryHash() { + return 31 * sql.queryHash() + columnMapping.queryHash(); + } + + /** + * Represents the sql part of the query. For parsed RawSql the sql is broken + * up so that Ebean can insert extra WHERE and HAVING expressions into the + * SQL. + */ + public static final class Sql implements Serializable { + + private static final long serialVersionUID = 1L; + + private final boolean parsed; + + private final String unparsedSql; + + private final String preFrom; + + private final String preWhere; + + private final boolean andWhereExpr; + + private final String preHaving; + + private final boolean andHavingExpr; + + private final String orderBy; + + private final boolean distinct; + + private final int queryHashCode; + + /** + * Construct for unparsed SQL. + */ + protected Sql(String unparsedSql) { + this.queryHashCode = unparsedSql.hashCode(); + this.parsed = false; + this.unparsedSql = unparsedSql; + this.preFrom = null; + this.preHaving = null; + this.preWhere = null; + this.andHavingExpr = false; + this.andWhereExpr = false; + this.orderBy = null; + this.distinct = false; + } + + /** + * Construct for parsed SQL. + */ + protected Sql(int queryHashCode, String preFrom, String preWhere, boolean andWhereExpr, + String preHaving, boolean andHavingExpr, + String orderBy, boolean distinct) { + + this.queryHashCode = queryHashCode; + this.parsed = true; + this.unparsedSql = null; + this.preFrom = preFrom; + this.preHaving = preHaving; + this.preWhere = preWhere; + this.andHavingExpr = andHavingExpr; + this.andWhereExpr = andWhereExpr; + this.orderBy = orderBy; + this.distinct = distinct; + } + + /** + * Return a hash for this query. + */ + public int queryHash() { + return queryHashCode; + } + + public String toString() { + if (!parsed) { + return "unparsed[" + unparsedSql + "]"; + } + return "select[" + preFrom + "] preWhere[" + preWhere + "] preHaving[" + preHaving + + "] orderBy[" + orderBy + "]"; + } + + public boolean isDistinct() { + return distinct; + } + + /** + * Return true if the SQL is left completely unmodified. + *

    + * This means Ebean can't add WHERE or HAVING expressions into the query - + * it will be left completely unmodified. + *

    + */ + public boolean isParsed() { + return parsed; + } + + /** + * Return the SQL when it is unparsed. + */ + public String getUnparsedSql() { + return unparsedSql; + } + + /** + * Return the SQL prior to FROM clause. + */ + public String getPreFrom() { + return preFrom; + } + + /** + * Return the SQL prior to WHERE clause. + */ + public String getPreWhere() { + return preWhere; + } + + /** + * Return true if there is already a WHERE clause and any extra where + * expressions start with AND. + */ + public boolean isAndWhereExpr() { + return andWhereExpr; + } + + /** + * Return the SQL prior to HAVING clause. + */ + public String getPreHaving() { + return preHaving; + } + + /** + * Return true if there is already a HAVING clause and any extra having + * expressions start with AND. + */ + public boolean isAndHavingExpr() { + return andHavingExpr; + } + + /** + * Return the SQL ORDER BY clause. + */ + public String getOrderBy() { + return orderBy; + } + + } + + /** + * Defines the column mapping for raw sql DB columns to bean properties. + */ + public static final class ColumnMapping implements Serializable { + + private static final long serialVersionUID = 1L; + + private final LinkedHashMap dbColumnMap; + + private final Map propertyMap; + private final Map propertyColumnMap; + + private final boolean parsed; + + private final boolean immutable; + + private final int queryHashCode; + + /** + * Construct from parsed sql where the columns have been identified. + */ + protected ColumnMapping(List columns) { + this.queryHashCode = 0; + this.immutable = false; + this.parsed = true; + this.propertyMap = null; + this.propertyColumnMap = null; + this.dbColumnMap = new LinkedHashMap(); + for (int i = 0; i < columns.size(); i++) { + Column c = columns.get(i); + dbColumnMap.put(c.getDbColumn(), c); + } + } + + /** + * Construct for unparsed sql. + */ + protected ColumnMapping() { + this.queryHashCode = 0; + this.immutable = false; + this.parsed = false; + this.propertyMap = null; + this.propertyColumnMap = null; + this.dbColumnMap = new LinkedHashMap(); + } + + /** + * Construct an immutable ColumnMapping based on collected information. + */ + protected ColumnMapping(boolean parsed, LinkedHashMap dbColumnMap) { + this.immutable = true; + this.parsed = parsed; + this.dbColumnMap = dbColumnMap; + + int hc = ColumnMapping.class.getName().hashCode(); + + HashMap pcMap = new HashMap(); + HashMap pMap = new HashMap(); + + for (Column c : dbColumnMap.values()) { + pMap.put(c.getPropertyName(), c.getDbColumn()); + pcMap.put(c.getPropertyName(), c); + + hc = 31 * hc + c.getPropertyName() == null ? 0 : c.getPropertyName().hashCode(); + hc = 31 * hc + c.getDbColumn() == null ? 0 : c.getDbColumn().hashCode(); + } + this.propertyMap = Collections.unmodifiableMap(pMap); + this.propertyColumnMap = Collections.unmodifiableMap(pcMap); + this.queryHashCode = hc; + } + + /** + * Creates an immutable copy of this ColumnMapping. + * + * @throws IllegalStateException + * when a propertyName has not been defined for a column. + */ + protected ColumnMapping createImmutableCopy() { + + for (Column c : dbColumnMap.values()) { + c.checkMapping(); + } + + return new ColumnMapping(parsed, dbColumnMap); + } + + protected void columnMapping(String dbColumn, String propertyName) { + + if (immutable) { + throw new IllegalStateException("Should never happen"); + } + if (!parsed) { + int pos = dbColumnMap.size(); + dbColumnMap.put(dbColumn, new Column(pos, dbColumn, null, propertyName)); + } else { + Column column = dbColumnMap.get(dbColumn); + if (column == null) { + String msg = "DB Column [" + dbColumn + "] not found in mapping. Expecting one of [" + + dbColumnMap.keySet() + "]"; + throw new IllegalArgumentException(msg); + } + column.setPropertyName(propertyName); + } + } + + /** + * Return the query hash for this column mapping. + */ + public int queryHash() { + if (queryHashCode == 0) { + throw new RuntimeException("Bug: queryHashCode == 0"); + } + return queryHashCode; + } + + /** + * Returns true if the Columns where supplied by parsing the sql select + * clause. + *

    + * In the case where the columns where parsed then we can do extra checks on + * the column mapping such as, is the column a valid one in the sql and + * whether all the columns in the sql have been mapped. + *

    + */ + public boolean isParsed() { + return parsed; + } + + /** + * Return the number of columns in this column mapping. + */ + public int size() { + return dbColumnMap.size(); + } + + /** + * Return the column mapping. + */ + protected Map mapping() { + return dbColumnMap; + } + + /** + * Return the mapping by DB column. + */ + public Map getMapping() { + return propertyMap; + } + + /** + * Return the index position by bean property name. + */ + public int getIndexPosition(String property) { + Column c = propertyColumnMap.get(property); + return c == null ? -1 : c.getIndexPos(); + } + + /** + * Return an iterator of the Columns. + */ + public Iterator getColumns() { + return dbColumnMap.values().iterator(); + } + + /** + * A Column of the RawSql that is mapped to a bean property (or ignored). + */ + public static class Column implements Serializable { + + private static final long serialVersionUID = 1L; + private final int indexPos; + private final String dbColumn; + + private final String dbAlias; + + private String propertyName; + + /** + * Construct a Column. + */ + public Column(int indexPos, String dbColumn, String dbAlias) { + this(indexPos, dbColumn, dbAlias, derivePropertyName(dbAlias, dbColumn)); + } + + private Column(int indexPos, String dbColumn, String dbAlias, String propertyName) { + this.indexPos = indexPos; + this.dbColumn = dbColumn; + this.dbAlias = dbAlias; + if (propertyName == null && dbAlias != null) { + this.propertyName = dbAlias; + } else { + this.propertyName = propertyName; + } + } + + private static String derivePropertyName(String dbAlias, String dbColumn) { + if (dbAlias != null) { + return dbAlias; + } + int dotPos = dbColumn.indexOf('.'); + if (dotPos > -1) { + dbColumn = dbColumn.substring(dotPos + 1); + } + return CamelCaseHelper.toCamelFromUnderscore(dbColumn); + } + + private void checkMapping() { + if (propertyName == null) { + String msg = "No propertyName defined (Column mapping) for dbColumn [" + dbColumn + "]"; + throw new IllegalStateException(msg); + } + } + + public String toString() { + return dbColumn + "->" + propertyName; + } + + /** + * Return the index position of this column. + */ + public int getIndexPos() { + return indexPos; + } + + /** + * Return the DB column name including table alias (if it has one). + */ + public String getDbColumn() { + return dbColumn; + } + + /** + * Return the DB column alias (if it has one). + */ + public String getDbAlias() { + return dbAlias; + } + + /** + * Return the bean property this column is mapped to. + */ + public String getPropertyName() { + return propertyName; + } + + private void setPropertyName(String propertyName) { + this.propertyName = propertyName; + } + + } + } +} diff --git a/src/main/java/com/avaje/ebean/RawSqlBuilder.java b/src/main/java/com/avaje/ebean/RawSqlBuilder.java new file mode 100644 index 000000000..7749829b2 --- /dev/null +++ b/src/main/java/com/avaje/ebean/RawSqlBuilder.java @@ -0,0 +1,104 @@ +package com.avaje.ebean; + +import com.avaje.ebean.RawSql.ColumnMapping; +import com.avaje.ebean.RawSql.Sql; + +/** + * Builds RawSql instances from a SQL string and column mappings. + *

    + * Note that RawSql can also be defined in ebean-orm.xml files and be used as a + * named query. + *

    + * + * @author rbygrave + * + * @see RawSql + */ +public class RawSqlBuilder { + + /** + * Special property name assigned to a DB column that should be ignored. + */ + public static final String IGNORE_COLUMN = "$$_IGNORE_COLUMN_$$"; + + private final Sql sql; + + private final ColumnMapping columnMapping; + + /** + * Return an unparsed RawSqlBuilder. Unlike a parsed one this query can not be + * modified - so no additional WHERE or HAVING expressions can be added to + * this query. + */ + public static RawSqlBuilder unparsed(String sql) { + + Sql s = new Sql(sql); + return new RawSqlBuilder(s, new ColumnMapping()); + } + + /** + * Return a RawSqlBuilder parsing the sql. + *

    + * The sql statement will be parsed so that Ebean can determine where it can + * insert additional WHERE or HAVING expressions. + *

    + *

    + * Additionally the selected columns are parsed to determine the column + * ordering. This also means additional checks can be made with the column + * mapping - specifically we can check that all columns are mapped and that + * correct column names are entered into the mapping. + *

    + */ + public static RawSqlBuilder parse(String sql) { + + Sql sql2 = DRawSqlParser.parse(sql); + String select = sql2.getPreFrom(); + + ColumnMapping mapping = DRawSqlColumnsParser.parse(select); + return new RawSqlBuilder(sql2, mapping); + } + + private RawSqlBuilder(Sql sql, ColumnMapping columnMapping) { + this.sql = sql; + this.columnMapping = columnMapping; + } + + /** + * Set the mapping of a DB Column to a bean property. + *

    + * For Unparsed SQL the columnMapping MUST be defined in the same order that + * the columns appear in the SQL statement. + *

    + * + * @param dbColumn + * the DB column that we are mapping to a bean property + * @param propertyName + * the bean property that we are mapping the DB column to. + */ + public RawSqlBuilder columnMapping(String dbColumn, String propertyName) { + columnMapping.columnMapping(dbColumn, propertyName); + return this; + } + + /** + * Ignore this DB column. It is not mapped to any bean property. + */ + public RawSqlBuilder columnMappingIgnore(String dbColumn) { + return columnMapping(dbColumn, IGNORE_COLUMN); + } + + /** + * Create the immutable RawSql object. Do this after all the column mapping + * has been defined. + */ + public RawSql create() { + return new RawSql(sql, columnMapping.createImmutableCopy()); + } + + /** + * Return the internal parsed Sql object (for testing). + */ + protected Sql getSql() { + return sql; + } +} diff --git a/src/main/java/com/avaje/ebean/SimpleTextParser.java b/src/main/java/com/avaje/ebean/SimpleTextParser.java new file mode 100644 index 000000000..99276f2b1 --- /dev/null +++ b/src/main/java/com/avaje/ebean/SimpleTextParser.java @@ -0,0 +1,177 @@ +package com.avaje.ebean; + +public class SimpleTextParser { + + private final String oql; + private final char[] chars; + private final int eof; + + private int pos; + private String word; + private String lowerWord; + + private int openParenthesisCount; + + public SimpleTextParser(String oql) { + this.oql = oql; + this.chars = oql.toCharArray(); + this.eof = oql.length(); + } + + public int getPos() { + return pos; + } + + public String getOql() { + return oql; + } + + public String getWord() { + return word; + } + + public String peekNextWord() { + int origPos = pos; + String nw = nextWordInternal(); + pos = origPos; + return nw; + } + + /** + * Match the current and the next word. + */ + public boolean isMatch(String lowerMatch, String nextWordMatch) { + + if (isMatch(lowerMatch)) { + String nw = peekNextWord(); + if (nw != null) { + nw = nw.toLowerCase(); + return nw.equals(nextWordMatch); + } + } + return false; + } + + public boolean isFinished() { + return word == null; + } + + public int findWordLower(String lowerMatch, int afterPos) { + this.pos = afterPos; + return findWordLower(lowerMatch); + } + + public int findWordLower(String lowerMatch) { + do { + if (nextWord() == null) { + return -1; + } + if (lowerMatch.equals(lowerWord)) { + return pos - lowerWord.length(); + } + } while (true); + } + + /** + * Match the current word. + */ + public boolean isMatch(String lowerMatch) { + return lowerMatch.equals(lowerWord); + } + + public String nextWord() { + word = nextWordInternal(); + if (word != null) { + lowerWord = word.toLowerCase(); + } + return word; + } + + private String nextWordInternal() { + trimLeadingWhitespace(); + if (pos >= eof) { + return null; + } + int start = pos; + if (chars[pos] == '(') { + moveToClose(); + } else { + moveToEndOfWord(); + } + return oql.substring(start, pos); + } + + private void moveToClose() { + + pos++; + openParenthesisCount = 0; + + for (; pos < eof; pos++) { + char c = chars[pos]; + if (c == '(') { + // count nested parenthesis + openParenthesisCount++; + + } else if (c == ')') { + if (openParenthesisCount > 0) { + // still in nested parenthesis + --openParenthesisCount; + } else { + // we have found the end + pos++; + return; + } + } + } + } + + private void moveToEndOfWord() { + char c = chars[pos]; + boolean isOperator = isOperator(c); + for (; pos < eof; pos++) { + c = chars[pos]; + if (isWordTerminator(c, isOperator)) { + return; + } + } + } + + private boolean isWordTerminator(char c, boolean isOperator) { + if (Character.isWhitespace(c)) { + return true; + } + if (isOperator(c)) { + return !isOperator; + } + if (c == '(') { + return true; + } + + return isOperator; + } + + private boolean isOperator(char c) { + switch (c) { + case '<': + return true; + case '>': + return true; + case '=': + return true; + case '!': + return true; + + default: + return false; + } + } + + private void trimLeadingWhitespace() { + for (; pos < eof; pos++) { + char c = chars[pos]; + if (!Character.isWhitespace(c)) { + break; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/SqlFutureList.java b/src/main/java/com/avaje/ebean/SqlFutureList.java new file mode 100644 index 000000000..6df27b705 --- /dev/null +++ b/src/main/java/com/avaje/ebean/SqlFutureList.java @@ -0,0 +1,47 @@ +package com.avaje.ebean; + +import java.util.List; +import java.util.concurrent.Future; + +/** + * The SqlFutureList represents the result of a background SQL query execution. + * + *

    + * It extends the java.util.concurrent.Future. + *

    + * + *
    + *  // create a query
    + * String sql = ... ;
    + * SqlQuery sqlQuery = Ebean.createSqlQuery(sql);
    + * 
    + *  // execute the query in a background thread
    + * SqlFutureList sqlFuture = sqlQuery.findFutureList();
    + * 
    + *  // do something else ... we will sleep
    + * Thread.sleep(3000);
    + * System.out.println("end of sleep");
    + * 
    + * if (!futureList.isDone()){
    + * 	// we can cancel the query execution
    + * 	futureList.cancel(true);
    + * }
    + * 
    + * System.out.println("and... done:"+futureList.isDone());
    + * 
    + * if (!futureList.isCancelled()){
    + * 	// wait for the query to finish and return the list
    + * 	List<SqlRow> list = futureList.get();
    + * 	System.out.println("list:"+list);
    + * }
    + * 
    + * 
    + * + * @author rob + * + */ +public interface SqlFutureList extends Future> { + + public SqlQuery getQuery(); + +} diff --git a/src/main/java/com/avaje/ebean/SqlQuery.java b/src/main/java/com/avaje/ebean/SqlQuery.java new file mode 100644 index 000000000..dea460cdf --- /dev/null +++ b/src/main/java/com/avaje/ebean/SqlQuery.java @@ -0,0 +1,150 @@ +package com.avaje.ebean; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Query object for performing native SQL queries that return SqlRow's. + *

    + * Firstly note that you can use your own sql queries with entity beans + * by using the SqlSelect annotation. This should be your first approach when + * wanting to use your own SQL queries. + *

    + *

    + * If ORM Mapping is too tight and constraining for your problem then SqlQuery + * could be a good approach. + *

    + *

    + * The returned SqlRow objects are similar to a LinkedHashMap with some type + * conversion support added. + *

    + * + *
    + * // its typically a good idea to use a named query
    + * // and put the sql in the orm.xml instead of in your code
    + * 
    + * String sql = "select id, name from customer where name like :name and status_code = :status";
    + * 
    + * SqlQuery sqlQuery = Ebean.createSqlQuery(sql);
    + * sqlQuery.setParameter("name", "Acme%");
    + * sqlQuery.setParameter("status", "ACTIVE");
    + * 
    + * // execute the query returning a List of MapBean objects
    + * List<SqlRow> list = sqlQuery.findList();
    + * 
    + * + */ +public interface SqlQuery extends Serializable { + + /** + * Cancel the query if support by the underlying database and driver. + *

    + * This must be called from a different thread to the one executing the query. + *

    + */ + public void cancel(); + + /** + * Execute the query returning a list. + */ + public List findList(); + + /** + * Execute the query returning a set. + */ + public Set findSet(); + + /** + * Execute the query returning a map. + */ + public Map findMap(); + + /** + * Execute the query returning a single row or null. + *

    + * If this query finds 2 or more rows then it will throw a + * PersistenceException. + *

    + */ + public SqlRow findUnique(); + + /** + * Execute find list SQL query in a background thread. + *

    + * This returns a Future object which can be used to cancel, check the + * execution status (isDone etc) and get the value (with or without a + * timeout). + *

    + * + * @return a Future object for the list result of the query + */ + public SqlFutureList findFutureList(); + + /** + * The same as bind for named parameters. + */ + public SqlQuery setParameter(String name, Object value); + + /** + * The same as bind for positioned parameters. + */ + public SqlQuery setParameter(int position, Object value); + + /** + * Set a listener to process the query on a row by row basis. + *

    + * It this case the rows are not loaded into the persistence context and + * instead can be processed by the query listener. + *

    + *

    + * Use this when you want to process a large query and do not want to hold the + * entire query result in memory. + *

    + */ + public SqlQuery setListener(SqlQueryListener queryListener); + + /** + * Set the index of the first row of the results to return. + */ + public SqlQuery setFirstRow(int firstRow); + + /** + * Set the maximum number of query results to return. + */ + public SqlQuery setMaxRows(int maxRows); + + /** + * Set the index after which fetching continues in a background thread. + */ + public SqlQuery setBackgroundFetchAfter(int backgroundFetchAfter); + + /** + * Set the column to use to determine the keys for a Map. + */ + public SqlQuery setMapKey(String mapKey); + + /** + * Set a timeout on this query. + *

    + * This will typically result in a call to setQueryTimeout() on a + * preparedStatement. If the timeout occurs an exception will be thrown - this + * will be a SQLException wrapped up in a PersistenceException. + *

    + * + * @param secs + * the query timeout limit in seconds. Zero means there is no limit. + */ + public SqlQuery setTimeout(int secs); + + /** + * A hint which for JDBC translates to the Statement.fetchSize(). + *

    + * Gives the JDBC driver a hint as to the number of rows that should be + * fetched from the database when more rows are needed for ResultSet. + *

    + */ + public SqlQuery setBufferFetchSizeHint(int bufferFetchSizeHint); + +} diff --git a/src/main/java/com/avaje/ebean/SqlQueryListener.java b/src/main/java/com/avaje/ebean/SqlQueryListener.java new file mode 100644 index 000000000..b6c50d1a7 --- /dev/null +++ b/src/main/java/com/avaje/ebean/SqlQueryListener.java @@ -0,0 +1,33 @@ +package com.avaje.ebean; + +/** + * Provides a mechanism for processing a SqlQuery one SqlRow at a time. + *

    + * This is useful when the query will return a large number of results and you + * want to process the beans one at a time rather than have all of the beans in + * memory at once. + *

    + * + *
    + * SqlQueryListener listener = ...;
    + *    
    + * SqlQuery query  = Ebean.createSqlQuery("my.large.query");
    + *    
    + * // set the listener that will process each row one at a time
    + * query.setListener(listener);
    + *    
    + * // execute the query. Note that the returned
    + * // list will be empty ... so don't bother assigning it...
    + * query.findList();
    + * 
    + */ +public interface SqlQueryListener { + + /** + * Process the bean that has just been read. + *

    + * Note this bean will not be added to the List Set or Map. + *

    + */ + public void process(SqlRow bean); +} diff --git a/src/main/java/com/avaje/ebean/SqlRow.java b/src/main/java/com/avaje/ebean/SqlRow.java new file mode 100644 index 000000000..9c279a3a8 --- /dev/null +++ b/src/main/java/com/avaje/ebean/SqlRow.java @@ -0,0 +1,168 @@ +package com.avaje.ebean; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Date; +import java.sql.Timestamp; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * Used to return raw SQL query results. + *

    + * Refer to {@link SqlQuery} for examples. + *

    + *

    + * There are convenience methods such as getInteger(), getBigDecimal() etc. The + * reason for these methods is that the values put into this map often come + * straight from the JDBC resultSet. Depending on the JDBC driver it may put a + * different type into a given property. For example an Integer, BigDecimal, + * Double could all be put into a property depending on the JDBC driver used. + * These convenience methods automatically convert the value as required + * returning the type you expect. + *

    + */ +public interface SqlRow extends Serializable, Map { + + /** + * Return the property names (String). + *

    + * Internally this uses LinkedHashMap and so the order of the property names + * should be predictable and ordered by the use of LinkedHashMap. + *

    + */ + public Iterator keys(); + + /** + * Remove a property from the map. Returns the value of the removed property. + */ + public Object remove(Object name); + + /** + * Return a property value by its name. + */ + public Object get(Object name); + + /** + * Set a value to a property. + */ + public Object put(String name, Object value); + + /** + * Exactly the same as the put method. + *

    + * I added this method because it seems more bean like to have get and set + * methods. + *

    + */ + public Object set(String name, Object value); + + /** + * Return a property as a Boolean. + */ + public Boolean getBoolean(String name); + + /** + * Return a property as a UUID. + */ + public UUID getUUID(String name); + + /** + * Return a property as an Integer. + */ + public Integer getInteger(String name); + + /** + * Return a property value as a BigDecimal. + */ + public BigDecimal getBigDecimal(String name); + + /** + * Return a property value as a Long. + */ + public Long getLong(String name); + + /** + * Return the property value as a Double. + */ + public Double getDouble(String name); + + /** + * Return the property value as a Float. + */ + public Float getFloat(String name); + + /** + * Return a property as a String. + */ + public String getString(String name); + + /** + * Return the property as a java.util.Date. + */ + public java.util.Date getUtilDate(String name); + + /** + * Return the property as a sql date. + */ + public Date getDate(String name); + + /** + * Return the property as a sql timestamp. + */ + public Timestamp getTimestamp(String name); + + /** + * String description of the underlying map. + */ + public String toString(); + + /** + * Clear the map. + */ + public void clear(); + + /** + * Returns true if the map contains the property. + */ + public boolean containsKey(Object key); + + /** + * Returns true if the map contains the value. + */ + public boolean containsValue(Object value); + + /** + * Returns the entrySet of the map. + */ + public Set> entrySet(); + + /** + * Returns true if the map is empty. + */ + public boolean isEmpty(); + + /** + * Returns the key set of the map. + */ + public Set keySet(); + + /** + * Put all the values from t into this map. + */ + public void putAll(Map t); + + /** + * Return the size of the map. + */ + public int size(); + + /** + * Return the values from this map. + */ + public Collection values(); + +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/SqlUpdate.java b/src/main/java/com/avaje/ebean/SqlUpdate.java new file mode 100644 index 000000000..aaa4833a8 --- /dev/null +++ b/src/main/java/com/avaje/ebean/SqlUpdate.java @@ -0,0 +1,141 @@ +package com.avaje.ebean; + +/** + * A SqlUpdate for executing insert update or delete statements. + *

    + * Provides a simple way to execute raw SQL insert update or delete statements + * without having to resort to JDBC. + *

    + *

    + * Supports the use of positioned or named parameters and can automatically + * notify Ebean of the table modified so that Ebean can maintain its cache. + *

    + *

    + * Note that {@link #setAutoTableMod(boolean)} and + * Ebean#externalModification(String, boolean, boolean, boolean)} can be to + * notify Ebean of external changes and enable Ebean to maintain it's "L2" + * server cache. + *

    + * + *
    + * // example that uses 'named' parameters 
    + * String s = "UPDATE f_topic set post_count = :count where id = :id"
    + * SqlUpdate update = Ebean.createSqlUpdate(s);
    + * update.setParameter("id", 1);
    + * update.setParameter("count", 50);
    + * 
    + * int modifiedCount = Ebean.execute(update);
    + * 
    + * String msg = "There where " + modifiedCount + "rows updated"
    + * 
    + * + * @see Update + * @see SqlQuery + * @see CallableSql + */ +public interface SqlUpdate { + + /** + * Execute the update returning the number of rows modified. + *

    + * After you have executed the SqlUpdate you can bind new variables using + * {@link #setParameter(String, Object)} etc and then execute the SqlUpdate + * again. + *

    + *

    + * For JDBC batch processing refer to + * {@link Transaction#setBatchMode(boolean)} and + * {@link Transaction#setBatchSize(int)}. + *

    + * + * @see com.avaje.ebean.Ebean#execute(SqlUpdate) + */ + public int execute(); + + /** + * Return true if eBean should automatically deduce the table modification + * information and process it. + *

    + * If this is true then cache invalidation and text index management are aware + * of the modification. + *

    + */ + public boolean isAutoTableMod(); + + /** + * Set this to false if you don't want eBean to automatically deduce the table + * modification information and process it. + *

    + * Set this to false if you don't want any cache invalidation or text index + * management to occur. You may do this when say you update only one column + * and you know that it is not important for cached objects or text indexes. + *

    + */ + public SqlUpdate setAutoTableMod(boolean isAutoTableMod); + + /** + * Return the label that can be seen in the transaction logs. + */ + public String getLabel(); + + /** + * Set a descriptive text that can be put into the transaction log. + *

    + * Useful when identifying the statement in the transaction log. + *

    + */ + public SqlUpdate setLabel(String label); + + /** + * Return the sql statement. + */ + public String getSql(); + + /** + * Return the timeout used to execute this statement. + */ + public int getTimeout(); + + /** + * Set the timeout in seconds. Zero implies no limit. + *

    + * This will set the query timeout on the underlying PreparedStatement. If the + * timeout expires a SQLException will be throw and wrapped in a + * PersistenceException. + *

    + */ + public SqlUpdate setTimeout(int secs); + + /** + * Set a parameter via its index position. + */ + public SqlUpdate setParameter(int position, Object value); + + /** + * Set a null parameter via its index position. Exactly the same as + * {@link #setNull(int, int)}. + */ + public SqlUpdate setNull(int position, int jdbcType); + + /** + * Set a null valued parameter using its index position. + */ + public SqlUpdate setNullParameter(int position, int jdbcType); + + /** + * Set a named parameter value. + */ + public SqlUpdate setParameter(String name, Object param); + + /** + * Set a named parameter that has a null value. Exactly the same as + * {@link #setNullParameter(String, int)}. + */ + public SqlUpdate setNull(String name, int jdbcType); + + /** + * Set a named parameter that has a null value. + */ + public SqlUpdate setNullParameter(String name, int jdbcType); + +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/Transaction.java b/src/main/java/com/avaje/ebean/Transaction.java new file mode 100644 index 000000000..14fb8821a --- /dev/null +++ b/src/main/java/com/avaje/ebean/Transaction.java @@ -0,0 +1,314 @@ +package com.avaje.ebean; + +import javax.persistence.OptimisticLockException; +import javax.persistence.PersistenceException; +import javax.persistence.RollbackException; +import java.sql.Connection; + +/** + * The Transaction object. Typically representing a JDBC or JTA transaction. + */ +public interface Transaction { + + /** + * Read Committed transaction isolation. Same as + * java.sql.Connection.TRANSACTION_READ_COMMITTED. + */ + public static final int READ_COMMITTED = java.sql.Connection.TRANSACTION_READ_COMMITTED; + + /** + * Read Uncommitted transaction isolation. Same as + * java.sql.Connection.TRANSACTION_READ_UNCOMMITTED. + */ + public static final int READ_UNCOMMITTED = java.sql.Connection.TRANSACTION_READ_UNCOMMITTED; + + /** + * Repeatable read transaction isolation. Same as + * java.sql.Connection.TRANSACTION_REPEATABLE_READ. + */ + public static final int REPEATABLE_READ = java.sql.Connection.TRANSACTION_REPEATABLE_READ; + + /** + * Serializable transaction isolation. Same as + * java.sql.Connection.TRANSACTION_SERIALIZABLE. + */ + public static final int SERIALIZABLE = java.sql.Connection.TRANSACTION_SERIALIZABLE; + + // /** + // * You can call this after the transaction commit to wait for any changes to + // * Lucene indexes to be made and committed. Note that generally this could + // * be some time so it is not expected that you call this method generally. + // */ + // public void waitForIndexUpdates(); + + /** + * Return true if this transaction is read only. + */ + public boolean isReadOnly(); + + /** + * Set whether this transaction should be readOnly. + */ + public void setReadOnly(boolean readOnly); + +// /** +// * Log a comment to the transaction log. +// */ +// public void log(String msg); + + /** + * Set the logLevel to use for this transaction. + */ + public void setLogLevel(LogLevel logLevel); + + /** + * Return the logLevel this transaction is using. + */ + public LogLevel getLogLevel(); + + /** + * Deprecated in favour of using {@link #setLogLevel} Set this to false to + * disable logging for this transaction. + * + * @deprecated + */ + public void setLoggingOn(boolean isLoggingOn); + + /** + * Commit the transaction. + */ + public void commit() throws RollbackException; + + /** + * Rollback the transaction. + */ + public void rollback() throws PersistenceException; + + /** + * Rollback the transaction specifying a throwable that caused the rollback to + * occur. + *

    + * If you are using transaction logging this will log the throwable in the + * transaction logs. + *

    + */ + public void rollback(Throwable e) throws PersistenceException; + + /** + * If the transaction is active then perform rollback. Otherwise do nothing. + */ + public void end() throws PersistenceException; + + /** + * Return true if the transaction is active. + */ + public boolean isActive(); + + /** + * Explicitly turn off or on the cascading nature of save() and delete(). This + * gives the developer exact control over what beans are saved and deleted + * rather than Ebean cascading detecting 'dirty/modified' beans etc. + *

    + * This is useful if you can getting back entity beans from a layer of code + * (potentially remote) and you prefer to have exact control. + *

    + *

    + * This may also be useful if you are using jdbc batching with jdbc drivers + * that do not support getGeneratedKeys. + *

    + */ + public void setPersistCascade(boolean persistCascade); + + /** + * Turn on or off statement batching. Statement batching can be transparent + * for drivers and databases that support getGeneratedKeys. Otherwise you may + * wish to specifically control when batching is used via this method. + *

    + * Refer to java.sql.PreparedStatement.addBatch(); + *

    + * Note that you may also wish to use the setPersistCascade method to stop + * save and delete cascade behaviour. You may do this to have full control + * over the order of execution rather than the normal cascading fashion. + *

    + *

    + * Note that the execution order in batch mode may be different from + * non batch mode execution order. Also note that insert behaviour + * may be different depending on the JDBC driver and its support for + * getGeneratedKeys. That is, for JDBC drivers that do not support + * getGeneratedKeys you may not get back the generated IDs (used for inserting + * associated detail beans etc). + *

    + *

    + * Calls to save(), delete(), insert() and execute() all support batch + * processing. This includes normal beans, MapBean, CallableSql and UpdateSql. + *

    + *

    + * The flushing of the batched statements is automatic but you can call + * batchFlush when you like. Note that flushing occurs when a query is + * executed or when you mix UpdateSql and CallableSql with save and delete of + * beans. + *

    + *

    + * Example: batch processing executing every 3 rows + *

    + * + *
    +   * String data = "This is a simple test of the batch processing"
    +   *     + " mode and the transaction execute batch method";
    +   * 
    +   * String[] da = data.split(" ");
    +   * 
    +   * String sql = "{call sp_t3(?,?)}";
    +   * 
    +   * CallableSql cs = new CallableSql(sql);
    +   * cs.registerOut(2, Types.INTEGER);
    +   * 
    +   * // (optional) inform eBean this stored procedure
    +   * // inserts into a table called sp_test
    +   * cs.addModification("sp_test", true, false, false);
    +   * 
    +   * Transaction t = Ebean.beginTransaction();
    +   * t.setBatchMode(true);
    +   * t.setBatchSize(3);
    +   * try {
    +   *   for (int i = 0; i < da.length;) {
    +   * 
    +   *     cs.setParameter(1, da[i]);
    +   *     Ebean.execute(cs);
    +   *   }
    +   * 
    +   *   // NB: commit implicitly flushes
    +   *   Ebean.commitTransaction();
    +   * 
    +   * } finally {
    +   *   Ebean.endTransaction();
    +   * }
    +   * 
    + * + */ + public void setBatchMode(boolean useBatch); + + /** + * Specify the number of statements before a batch is flushed automatically. + */ + public void setBatchSize(int batchSize); + + /** + * Specify if you want batched inserts to use getGeneratedKeys. + *

    + * By default batched inserts will try to use getGeneratedKeys if it is + * supported by the underlying jdbc driver and database. + *

    + *

    + * You may want to turn getGeneratedKeys off when you are inserting a large + * number of objects and you don't care about getting back the ids. + *

    + */ + public void setBatchGetGeneratedKeys(boolean getGeneratedKeys); + + /** + * By default when mixing UpdateSql (or CallableSql) with Beans the batch is + * automatically flushed when you change (between persisting beans and + * executing UpdateSql or CallableSql). + *

    + * If you want to execute both WITHOUT having the batch automatically flush + * you need to call this with batchFlushOnMixed = false. + *

    + *

    + * Note that UpdateSql and CallableSql are ALWAYS executed first (before the + * beans are executed). This is because the UpdateSql and CallableSql have + * already been bound to their PreparedStatements. The beans on the other hand + * have a 2 step process (delayed binding). + *

    + */ + public void setBatchFlushOnMixed(boolean batchFlushOnMixed); + + /** + * By default executing a query will automatically flush any batched + * statements (persisted beans, executed UpdateSql etc). + *

    + * Calling this method with batchFlushOnQuery = false means that you can + * execute a query and the batch will not be automatically flushed. + *

    + */ + public void setBatchFlushOnQuery(boolean batchFlushOnQuery); + + /** + * Return true if the batch (of persisted beans or executed UpdateSql etc) + * should be flushed prior to executing a query. + *

    + * The default is for this to be true. + *

    + */ + public boolean isBatchFlushOnQuery(); + + /** + * The batch will be flushing automatically but you can use this to explicitly + * flush the batch if you like. + *

    + * Flushing occurs automatically when: + *

    + *
      + *
    • the batch size is reached
    • + *
    • A query is executed on the same transaction
    • + *
    • UpdateSql or CallableSql are mixed with bean save and delete
    • + *
    + */ + public void flushBatch() throws PersistenceException, OptimisticLockException; + + /** + * Deprecated in favour of {@link #flushBatch()}. + *

    + * Exactly the same as flushBatch. Deprecated as a name change. + *

    + * + * @deprecated Please use flushBatch + */ + public void batchFlush() throws PersistenceException, OptimisticLockException; + + /** + * Return the underlying Connection object. + *

    + * Useful where a Developer wishes to use the JDBC API directly. Note that the + * commit() rollback() and end() methods on the Transaction should still be + * used. Calling these methods on the Connection would be a big no no unless + * you know what you are doing. + *

    + *

    + * Examples of when a developer may wish to use the connection directly are: + * Savepoints, advanced CLOB BLOB use and advanced stored procedure calls. + *

    + */ + public Connection getConnection(); + + /** + * Add table modification information to the TransactionEvent. + *

    + * Use this in conjunction with getConnection() and raw JDBC. + *

    + *

    + * This effectively informs Ebean of the data that has been changed by the + * transaction and this information is normally automatically handled by Ebean + * when you save entity beans or use UpdateSql etc. + *

    + *

    + * If you use raw JDBC then you can use this method to inform Ebean for the + * tables that have been modified. Ebean uses this information to keep its + * caches in synch and maintain text indexes. + *

    + */ + public void addModification(String tableName, boolean inserts, boolean updates, boolean deletes); + + /** + * Add an arbitrary user object to the transaction. The objects added have no + * impact on any internals of ebena and are solely meant as a convenient + * method push user information to e.g. the + * {@link com.avaje.ebean.event.TransactionEventListener}. + */ + public void putUserObject(String name, Object value); + + /** + * Get an object added with {@link #putUserObject(String, Object)}. + */ + public Object getUserObject(String name); +} diff --git a/src/main/java/com/avaje/ebean/TxCallable.java b/src/main/java/com/avaje/ebean/TxCallable.java new file mode 100644 index 000000000..574960958 --- /dev/null +++ b/src/main/java/com/avaje/ebean/TxCallable.java @@ -0,0 +1,45 @@ +package com.avaje.ebean; + +/** + * Execute a TxCallable in a Transaction scope. + *

    + * Use this with the {@link Ebean#execute(TxCallable)} method. + *

    + *

    + * Note that this is basically the same as TxRunnable except that it returns an + * Object (and you specify the return type via generics). + *

    + *

    + * See also {@link TxRunnable}. + *

    + * + *
    + * Ebean.execute(new TxCallable<String>() {
    + *   public String call() {
    + *     User u1 = Ebean.find(User.class, 1);
    + *     User u2 = Ebean.find(User.class, 2);
    + * 
    + *     u1.setName("u1 mod");
    + *     u2.setName("u2 mod");
    + * 
    + *     Ebean.save(u1);
    + *     Ebean.save(u2);
    + * 
    + *     return u1.getEmail();
    + *   }
    + * });
    + * 
    + * + * @see TxRunnable + */ +public interface TxCallable { + + /** + * Execute the method within a transaction scope returning the result. + *

    + * If you do not want to return a result you should look to use TxRunnable + * instead. + *

    + */ + public T call(); +} diff --git a/src/main/java/com/avaje/ebean/TxIsolation.java b/src/main/java/com/avaje/ebean/TxIsolation.java new file mode 100644 index 000000000..5bd71e6d7 --- /dev/null +++ b/src/main/java/com/avaje/ebean/TxIsolation.java @@ -0,0 +1,100 @@ +package com.avaje.ebean; + +import java.sql.Connection; + +/** + * The Transaction Isolation levels. + *

    + * These match those of java.sql.Connection with the addition of DEFAULT which + * implies the configured default of the DataSource. + *

    + *

    + * This can be used with TxScope to define transactional scopes to execute + * method within. + *

    + * + * @see TxScope + */ +public enum TxIsolation { + + /** + * Read Committed Isolation level. This is typically the default for most + * configurations. + */ + READ_COMMITED(Connection.TRANSACTION_READ_COMMITTED), + + /** + * Read uncommitted Isolation level. + */ + READ_UNCOMMITTED(Connection.TRANSACTION_READ_UNCOMMITTED), + + /** + * Repeatable Read Isolation level. + */ + REPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ), + + /** + * Serializable Isolation level. + */ + SERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE), + + /** + * No Isolation level. + */ + NONE(Connection.TRANSACTION_NONE), + + /** + * The default isolation level. This typically means the default that the + * DataSource is using or configured to use. + */ + DEFAULT(-1); + + final int level; + + private TxIsolation(int level) { + this.level = level; + } + + /** + * Return the level as per java.sql.Connection. + *

    + * Note that -1 denotes the default isolation level. + *

    + */ + public int getLevel() { + return level; + } + + /** + * Return the TxIsolation given the java.sql.Connection isolation level. + *

    + * Note that -1 denotes the default isolation level. + *

    + */ + public static TxIsolation fromLevel(int connectionIsolationLevel) { + + switch (connectionIsolationLevel) { + case Connection.TRANSACTION_READ_UNCOMMITTED: + return TxIsolation.READ_UNCOMMITTED; + + case Connection.TRANSACTION_READ_COMMITTED: + return TxIsolation.READ_COMMITED; + + case Connection.TRANSACTION_REPEATABLE_READ: + return TxIsolation.REPEATABLE_READ; + + case Connection.TRANSACTION_SERIALIZABLE: + return TxIsolation.SERIALIZABLE; + + case Connection.TRANSACTION_NONE: + return TxIsolation.NONE; + + case -1: + return TxIsolation.DEFAULT; + + default: + throw new RuntimeException("Unknown isolation level " + connectionIsolationLevel); + } + + } +} diff --git a/src/main/java/com/avaje/ebean/TxRunnable.java b/src/main/java/com/avaje/ebean/TxRunnable.java new file mode 100644 index 000000000..2821882e0 --- /dev/null +++ b/src/main/java/com/avaje/ebean/TxRunnable.java @@ -0,0 +1,39 @@ +package com.avaje.ebean; + +/** + * Execute a TxRunnable in a Transaction scope. + *

    + * Use this with the {@link Ebean#execute(TxRunnable)} method. + *

    + *

    + * See also {@link TxCallable}. + *

    + * + *
    + * 
    + * // this run method runs in a transaction scope
    + * // which by default is TxScope.REQUIRED
    + * 
    + * Ebean.execute(new TxRunnable() {
    + *   public void run() {
    + *     User u1 = Ebean.find(User.class, 1);
    + *     User u2 = Ebean.find(User.class, 2);
    + * 
    + *     u1.setName("u1 mod");
    + *     u2.setName("u2 mod");
    + * 
    + *     Ebean.save(u1);
    + *     Ebean.save(u2);
    + *   }
    + * });
    + * 
    + * + * @see TxCallable + */ +public interface TxRunnable { + + /** + * Run the method in a transaction sope. + */ + public void run(); +} diff --git a/src/main/java/com/avaje/ebean/TxScope.java b/src/main/java/com/avaje/ebean/TxScope.java new file mode 100644 index 000000000..286f2fd75 --- /dev/null +++ b/src/main/java/com/avaje/ebean/TxScope.java @@ -0,0 +1,231 @@ +package com.avaje.ebean; + +import java.util.ArrayList; + +/** + * Holds the definition of how a transactional method should run. + *

    + * This information matches the features of the Transactional annotation. You + * can use it directly with TxRunnable or TxCallable via + * {@link Ebean#execute(TxScope, TxCallable)} or + * {@link Ebean#execute(TxScope, TxRunnable)}. + *

    + *

    + * This object is used internally with the enhancement of a method with + * Transactional annotation. + *

    + * + * @see TxCallable + * @see TxRunnable + * @see Ebean#execute(TxScope, TxCallable) + * @see Ebean#execute(TxScope, TxRunnable) + */ +public final class TxScope { + + TxType type; + + String serverName; + + TxIsolation isolation; + + boolean readOnly; + + ArrayList> rollbackFor; + + ArrayList> noRollbackFor; + + /** + * Helper method to create a TxScope with REQUIRES. + */ + public static TxScope required() { + return new TxScope(TxType.REQUIRED); + } + + /** + * Helper method to create a TxScope with REQUIRES_NEW. + */ + public static TxScope requiresNew() { + return new TxScope(TxType.REQUIRES_NEW); + } + + /** + * Helper method to create a TxScope with MANDATORY. + */ + public static TxScope mandatory() { + return new TxScope(TxType.MANDATORY); + } + + /** + * Helper method to create a TxScope with SUPPORTS. + */ + public static TxScope supports() { + return new TxScope(TxType.SUPPORTS); + } + + /** + * Helper method to create a TxScope with NOT_SUPPORTED. + */ + public static TxScope notSupported() { + return new TxScope(TxType.NOT_SUPPORTED); + } + + /** + * Helper method to create a TxScope with NEVER. + */ + public static TxScope never() { + return new TxScope(TxType.NEVER); + } + + /** + * Create a REQUIRED transaction scope. + */ + public TxScope() { + this.type = TxType.REQUIRED; + } + + /** + * Create with a given transaction scope type. + */ + public TxScope(TxType type) { + this.type = type; + } + + /** + * Describes this TxScope instance. + */ + public String toString() { + return "TxScope[" + type + "] readOnly[" + readOnly + "] isolation[" + isolation + + "] serverName[" + serverName + + "] rollbackFor[" + rollbackFor + "] noRollbackFor[" + noRollbackFor + "]"; + } + + /** + * Return the transaction type. + */ + public TxType getType() { + return type; + } + + /** + * Set the transaction type. + */ + public TxScope setType(TxType type) { + this.type = type; + return this; + } + + /** + * Return if the transaction should be treated as read only. + */ + public boolean isReadonly() { + return readOnly; + } + + /** + * Set if the transaction should be treated as read only. + */ + public TxScope setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + return this; + } + + /** + * Return the Isolation level this transaction should run with. + */ + public TxIsolation getIsolation() { + return isolation; + } + + /** + * Set the transaction isolation level this transaction should run with. + */ + public TxScope setIsolation(TxIsolation isolation) { + this.isolation = isolation; + return this; + } + + /** + * Return the serverName for this transaction. If this is null then the + * default server (default DataSource) will be used. + */ + public String getServerName() { + return serverName; + } + + /** + * Set the serverName (DataSource name) for which this transaction will be. If + * the serverName is not specified (left null) then the default server will be + * used. + */ + public TxScope setServerName(String serverName) { + this.serverName = serverName; + return this; + } + + /** + * Return the throwable's that should cause a rollback. + */ + public ArrayList> getRollbackFor() { + return rollbackFor; + } + + /** + * Set a Throwable that should explicitly cause a rollback. + */ + public TxScope setRollbackFor(Class rollbackThrowable) { + if (rollbackFor == null) { + rollbackFor = new ArrayList>(2); + } + rollbackFor.add(rollbackThrowable); + return this; + } + + /** + * Set multiple throwable's that will cause a rollback. + */ + @SuppressWarnings("unchecked") + public TxScope setRollbackFor(Class[] rollbackThrowables) { + if (rollbackFor == null) { + rollbackFor = new ArrayList>(rollbackThrowables.length); + } + for (int i = 0; i < rollbackThrowables.length; i++) { + rollbackFor.add((Class) rollbackThrowables[i]); + } + return this; + } + + /** + * Return the throwable's that should NOT cause a rollback. + */ + public ArrayList> getNoRollbackFor() { + return noRollbackFor; + } + + /** + * Add a Throwable to a list that will NOT cause a rollback. You are able to + * call this method multiple times with different throwable's and they will + * added to a list. + */ + public TxScope setNoRollbackFor(Class noRollback) { + if (noRollbackFor == null) { + noRollbackFor = new ArrayList>(2); + } + this.noRollbackFor.add(noRollback); + return this; + } + + /** + * Set multiple throwable's that will NOT cause a rollback. + */ + @SuppressWarnings("unchecked") + public TxScope setNoRollbackFor(Class[] noRollbacks) { + if (noRollbackFor == null) { + noRollbackFor = new ArrayList>(noRollbacks.length); + } + for (int i = 0; i < noRollbacks.length; i++) { + noRollbackFor.add((Class) noRollbacks[i]); + } + return this; + } + +} diff --git a/src/main/java/com/avaje/ebean/TxType.java b/src/main/java/com/avaje/ebean/TxType.java new file mode 100644 index 000000000..812baa35a --- /dev/null +++ b/src/main/java/com/avaje/ebean/TxType.java @@ -0,0 +1,50 @@ +package com.avaje.ebean; + +/** + * Used to define the transactional scope for executing a method. Matches the + * types defined in the EJB TransactionAttributeType. + *

    + * Used with the Transactional annotation and the {@link TxScope} with + * {@link Ebean#execute(TxScope, TxCallable)} and + * {@link Ebean#execute(TxScope, TxRunnable)}. + *

    + * + * @see TxScope + */ +public enum TxType { + + /** + * Uses an existing transaction and if none exists will starts a new + * Transaction. This is the default. + */ + REQUIRED, + + /** + * A transaction MUST already have been started. Throws + * TransactionRequiredException. + */ + MANDATORY, + + /** + * Uses the existing transaction if one exists, otherwise the method does not + * run with a transaction. Used this with caution. + */ + SUPPORTS, + + /** + * Always start a new transaction. Suspend an existing once if required. + */ + REQUIRES_NEW, + + /** + * Suspends an existing transaction if required. Method runs without a + * transaction. + */ + NOT_SUPPORTED, + + /** + * If there is an existing transaction throws an Exception. Method runs + * without a transaction. + */ + NEVER; +} diff --git a/src/main/java/com/avaje/ebean/Update.java b/src/main/java/com/avaje/ebean/Update.java new file mode 100644 index 000000000..cb12e1547 --- /dev/null +++ b/src/main/java/com/avaje/ebean/Update.java @@ -0,0 +1,169 @@ +package com.avaje.ebean; + +/** + * An Insert Update or Delete statement. + *

    + * Generally a named update will be defined on the entity bean. This will take + * the form of either an actual sql insert update delete statement or a similar + * statement with bean name and property names in place of database table and + * column names. The statement will likely include named parameters. + *

    + *

    + * The following is an example of named updates on an entity bean. + *

    + * + *
    + *  ...
    + * @NamedUpdates(value = {
    + *   @NamedUpdate(
    + *      name = "setTitle", 
    + *      notifyCache = false, 
    + *      update = "update topic set title = :title, postCount = :count where id = :id"),
    + *  @NamedUpdate(
    + *      name = "setPostCount", 
    + *      notifyCache = false, 
    + *      update = "update f_topic set post_count = :postCount where id = :id"),
    + *  @NamedUpdate(
    + *      name = "incrementPostCount", 
    + *      notifyCache = false, 
    + *      update = "update Topic set postCount = postCount + 1 where id = :id") 
    + *      //update = "update f_topic set post_count = post_count + 1 where id = :id") 
    + *  })
    + * @Entity
    + * @Table(name = "f_topic")
    + * public class Topic {
    + *  ...
    + * 
    + * + *

    + * The following show code that would use a named update on the Topic entity + * bean. + *

    + * + *
    + * Update<Topic> update = Ebean.createUpdate(Topic.class, "incrementPostCount");
    + * update.setParameter("id", 1);
    + * int rows = update.execute();
    + * 
    + * + * @param + * the type of entity beans inserted updated or deleted + */ +public interface Update { + + /** + * Return the name if it is a named update. + */ + public String getName(); + + /** + * Set this to false if you do not want the cache to invalidate related + * objects. + *

    + * If you don't set this Ebean will automatically invalidate the appropriate + * parts of the "L2" server cache. + *

    + */ + public Update setNotifyCache(boolean notifyCache); + + /** + * Set a timeout for statement execution. + *

    + * This will typically result in a call to setQueryTimeout() on a + * preparedStatement. If the timeout occurs an exception will be thrown - this + * will be a SQLException wrapped up in a PersistenceException. + *

    + * + * @param secs + * the timeout in seconds. Zero implies unlimited. + */ + public Update setTimeout(int secs); + + /** + * Execute the statement returning the number of rows modified. + */ + public int execute(); + + /** + * Set an ordered bind parameter. + *

    + * position starts at value 1 (not 0) to be consistent with PreparedStatement. + *

    + *

    + * Set a value for each ? you have in the sql. + *

    + * + * @param position + * the index position of the parameter starting with 1. + * @param value + * the parameter value to bind. + */ + public Update set(int position, Object value); + + /** + * Set and ordered bind parameter (same as bind). + * + * @param position + * the index position of the parameter starting with 1. + * @param value + * the parameter value to bind. + */ + public Update setParameter(int position, Object value); + + /** + * Set an ordered parameter that is null. The JDBC type of the null must be + * specified. + *

    + * position starts at value 1 (not 0) to be consistent with PreparedStatement. + *

    + */ + public Update setNull(int position, int jdbcType); + + /** + * Set an ordered parameter that is null (same as bind). + */ + public Update setNullParameter(int position, int jdbcType); + + /** + * Set a named parameter. Named parameters have a colon to prefix the name. + *

    + * A more succinct version of setParameter() to be consistent with Query. + *

    + * + * @param name + * the parameter name. + * @param value + * the parameter value. + */ + public Update set(String name, Object value); + + /** + * Bind a named parameter (same as bind). + */ + public Update setParameter(String name, Object param); + + /** + * Set a named parameter that is null. The JDBC type of the null must be + * specified. + *

    + * A more succinct version of setNullParameter(). + *

    + * + * @param name + * the parameter name. + * @param jdbcType + * the type of the property being bound. + */ + public Update setNull(String name, int jdbcType); + + /** + * Bind a named parameter that is null (same as bind). + */ + public Update setNullParameter(String name, int jdbcType); + + /** + * Return the sql that is actually executed. + */ + public String getGeneratedSql(); + +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/ValuePair.java b/src/main/java/com/avaje/ebean/ValuePair.java new file mode 100644 index 000000000..e1a89caad --- /dev/null +++ b/src/main/java/com/avaje/ebean/ValuePair.java @@ -0,0 +1,34 @@ +package com.avaje.ebean; + +/** + * Holds two values as the result of a difference comparison. + */ +public class ValuePair { + + final Object value1; + + final Object value2; + + public ValuePair(Object value1, Object value2) { + this.value1 = value1; + this.value2 = value2; + } + + /** + * Return the first value. + */ + public Object getValue1() { + return value1; + } + + /** + * Return the second value. + */ + public Object getValue2() { + return value2; + } + + public String toString() { + return value1 + "," + value2; + } +} diff --git a/src/main/java/com/avaje/ebean/annotation/CacheStrategy.java b/src/main/java/com/avaje/ebean/annotation/CacheStrategy.java new file mode 100644 index 000000000..8551a7bc9 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/CacheStrategy.java @@ -0,0 +1,64 @@ +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; + +import com.avaje.ebean.Query; +import com.avaje.ebean.Query.UseIndex; + +/** + * Specify the default cache use specific entity type. + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface CacheStrategy { + + /** + * When set to true the bean cache will be used unless explicitly stated not + * to in a query via {@link Query#setUseCache(boolean)}. + */ + boolean useBeanCache() default true; + + /** + * A single property that is a natural unique identifier for the bean. + *

    + * When a findUnique query is used with this property as the sole expression + * then there will be a lookup into the L2 natural key cache. + *

    + */ + String naturalKey() default ""; + + /** + * When set to true the beans returned from a query will default to be + * readOnly. + *

    + * If the bean is readOnly and has no relationships then it may be sharable. + *

    + *

    + * If you try to modify a readOnly bean it will throw an + * IllegalStateException. + *

    + */ + boolean readOnly() default false; + + /** + * Specify a query that can be used to warm the cache. + *

    + * All the beans fetched by this query will be loaded into the bean cache and + * the query itself will be loaded into the query cache. + *

    + *

    + * The warming query will typically be executed at startup time after a short + * delay (defaults to a 30 seconds delay). + *

    + */ + String warmingQuery() default ""; + + /** + * Default setting for using a text index if it has been defined on this bean + * type. + */ + UseIndex useIndex() default UseIndex.DEFAULT; +}; diff --git a/src/main/java/com/avaje/ebean/annotation/CacheTuning.java b/src/main/java/com/avaje/ebean/annotation/CacheTuning.java new file mode 100644 index 000000000..b03d11050 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/CacheTuning.java @@ -0,0 +1,47 @@ +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; + +/** + * Specify cache tuning for a specific entity type. + *

    + * If this is not specified then the system default settings are used. + *

    + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface CacheTuning { + + /** + * The maximum size for the cache. + *

    + * This defaults to 0 which means unlimited. + *

    + */ + int maxSize() default 0; + + /** + * The maximum time (in seconds) that a cache entry is allowed to stay in the + * cache when it has not been accessed. + *

    + * This defaults to 0 which means unlimited. + *

    + */ + int maxIdleSecs() default 0; + + /** + * The maximum time (in seconds) a cache entry is allowed to stay in the + * cache. + *

    + * This is not generally required as the cache entries are automatically + * evicted when related data changes are committed. + *

    + *

    + * This defaults to 0 which means unlimited. + *

    + */ + int maxSecsToLive() default 0; +}; diff --git a/src/main/java/com/avaje/ebean/annotation/ConcurrencyMode.java b/src/main/java/com/avaje/ebean/annotation/ConcurrencyMode.java new file mode 100644 index 000000000..59730225c --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/ConcurrencyMode.java @@ -0,0 +1,22 @@ +package com.avaje.ebean.annotation; + +/** + * Optimistic concurrency mode used for updates and deletes. + */ +public enum ConcurrencyMode { + + /** + * No concurrency checking. + */ + NONE, + + /** + * Use a version column. + */ + VERSION, + + /** + * Use all the columns (except Lobs). + */ + ALL +} diff --git a/src/main/java/com/avaje/ebean/annotation/CreatedTimestamp.java b/src/main/java/com/avaje/ebean/annotation/CreatedTimestamp.java new file mode 100644 index 000000000..0d688cd93 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/CreatedTimestamp.java @@ -0,0 +1,31 @@ +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; + +/** + * For a timestamp property that is set to the datetime when the entity is + * created/inserted. + *

    + * An alternative to using this annotation would be to use insertable=false, + * updateable=false with @Column and have the DB insert the current time + * (default value on the DB column is SYSTIME etc). + *

    + *

    + * The downside to this approach is that the inserted entity does not have the + * timestamp value after the insert has occurred. You need to fetch the entity + * back to get the inserted timestamp if you want to used it. + *

    + * + *
    + * @Column(insertable = false, updateable = false)
    + * Timestamp cretimestamp;
    + * 
    + */ +@Target({ ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface CreatedTimestamp { + +}; diff --git a/src/main/java/com/avaje/ebean/annotation/EmbeddedColumns.java b/src/main/java/com/avaje/ebean/annotation/EmbeddedColumns.java new file mode 100644 index 000000000..7393bb5eb --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/EmbeddedColumns.java @@ -0,0 +1,31 @@ +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; + +/** + * Specify property name to db column mapping for Embedded beans. + *

    + * This is designed to be easier to use than the AttributeOverride annotation in + * standard JPA. + *

    + */ +@Target({ ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface EmbeddedColumns { + + /** + * A list of property names mapped to DB columns. + *

    + * For example currency=IN_CURR, amount=IN_AMOUNT + *

    + *

    + * Where currency and amount are properties and IN_CURR and IN_AMOUNT are the + * respective DB columns these properties will be mapped to. + *

    + */ + String columns() default ""; + +}; diff --git a/src/main/java/com/avaje/ebean/annotation/Encrypted.java b/src/main/java/com/avaje/ebean/annotation/Encrypted.java new file mode 100644 index 000000000..b6c2aff78 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/Encrypted.java @@ -0,0 +1,24 @@ +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; + +/** + * Specify that the property is stored in encrypted form. + */ +@Target({ ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Encrypted { + + /** + * When true try to use DB encryption rather than local java encryption. + */ + boolean dbEncryption() default true; + + /** + * Used to specify the DB column length. + */ + int dbLength() default 0; +}; diff --git a/src/main/java/com/avaje/ebean/annotation/EntityConcurrencyMode.java b/src/main/java/com/avaje/ebean/annotation/EntityConcurrencyMode.java new file mode 100644 index 000000000..e389c346a --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/EntityConcurrencyMode.java @@ -0,0 +1,19 @@ +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; + +/** + * Specify explicit ConcurrencyMode for entity bean. + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface EntityConcurrencyMode { + + /** + * The ConcurrencyMode value. + */ + ConcurrencyMode value(); +} diff --git a/src/main/java/com/avaje/ebean/annotation/EnumMapping.java b/src/main/java/com/avaje/ebean/annotation/EnumMapping.java new file mode 100644 index 000000000..8556527fb --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/EnumMapping.java @@ -0,0 +1,90 @@ +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; + +/** + * For mapping the values of an Enum to and from Database values. + *

    + * Also refer to the {@link EnumValue} approach which probably the preferred now + * (preferred over using this EnumMapping annotation). + *

    + *

    + * Both of the approaches defined in the JPA have significant problems!!! + *

    + *

    + * Using the ordinal value is VERY RISKY because that depends on the compile + * order of the enum values. Aka if you change the order of the enum values you + * have changed their ordinal values and now your DB values are WRONG - a HUGE + * disaster!!!. + *

    + *

    + * Using the String values of enums is fairly restrictive because in a Database + * these values are usually truncated into short codes (e.g. "A" short for + * "ACTIVE") so space used in the database is minimised. Making your enum names + * match the database values would give them very short less meaningful names - + * not a great solution. + *

    + *

    + * You can use this annotation to control the mapping of your enums to database + * values. + *

    + *

    + * The design of this using nameValuePairs is not optimal for safety or + * refactoring so if you have a better solution I'm all ears. The other + * solutions would probably involve modifying each enumeration with a method + * which may be ok. + *

    + *

    + * An example mapping the UserState enum. + *

    + * + *
    + * ...
    + * @EnumMapping(nameValuePairs="NEW=N, ACTIVE=A, INACTIVE=I")
    + *  public enum UserState {
    + *  NEW,
    + *  ACTIVE,
    + *  INACTIVE;
    + *  }
    + * 
    + * + * @see EnumValue + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface EnumMapping { + + /** + * A comma delimited list of name=value pairs. + *

    + * e.g. "ACTIVE=A, INACTIVE=I, NEW=N". + *

    + *

    + * Where ACTIVE, INACTIVE and NEW are the enumeration values and "A", "I" and + * "N" are the database values. + *

    + *

    + * This is not really an optimal approach so if you have a better one I'm all + * ears - thanks. + *

    + */ + String nameValuePairs(); + + /** + * Defaults to mapping values to database VARCHAR type. If this is set to true + * then the values will be converted to INTEGER and mapped to the database + * integer type. + *

    + * e.g. "ACTIVE=1, INACTIVE=0, NEW=2". + *

    + */ + boolean integerType() default false; + + /** + * The length of DB column if mapping to string values. + */ + int length() default 0; +}; diff --git a/src/main/java/com/avaje/ebean/annotation/EnumValue.java b/src/main/java/com/avaje/ebean/annotation/EnumValue.java new file mode 100644 index 000000000..de4422e74 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/EnumValue.java @@ -0,0 +1,46 @@ +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; + +/** + * Enables you to specify a value to use to persist for an enum value. + * + *
    + * public enum Status {
    + *   @EnumValue("N")
    + *   NEW,
    + * 
    + *   @EnumValue("A")
    + *   ACTIVE,
    + * 
    + *   @EnumValue("I")
    + *   INACTIVE,
    + * }
    + * 
    + * 
    + *

    + * This is an alternative to using the JPA standard approach or Ebean's + * {@link EnumMapping} annotation. + *

    + *

    + * Note that if all the EnumValue values are parsable as Integers then Ebean + * will persist and fetch them as integers - otherwise they will be persisted + * and fetched as strings. + *

    + */ +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface EnumValue { + + /** + * Specify the value to persist for a specific enum value. + *

    + * If all the values are parsable as Integers then Ebean will persist and + * fetch them as integers rather than strings. + *

    + */ + String value(); +}; diff --git a/src/main/java/com/avaje/ebean/annotation/Expose.java b/src/main/java/com/avaje/ebean/annotation/Expose.java new file mode 100644 index 000000000..a81c6bf51 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/Expose.java @@ -0,0 +1,28 @@ +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; + +/* + Copied from gson!!! + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Expose { + + /** + * If {@code true}, the field marked with this annotation is written out in the JSON while + * serializing. If {@code false}, the field marked with this annotation is skipped from the + * serialized output. Defaults to {@code true}. + */ + public boolean serialize() default true; + + /** + * If {@code true}, the field marked with this annotation is deserialized from the JSON. + * If {@code false}, the field marked with this annotation is skipped during deserialization. + * Defaults to {@code true}. + */ + public boolean deserialize() default true; +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/annotation/Formula.java b/src/main/java/com/avaje/ebean/annotation/Formula.java new file mode 100644 index 000000000..f33ca8e34 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/Formula.java @@ -0,0 +1,103 @@ +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; + +import com.avaje.ebean.Query; + +/** + * Assign to a property to be based on a SQL formula. + *

    + * This is typically a SQL Literal value, SQL case statement, SQL function or + * similar. + *

    + *

    + * Any property based on a formula becomes a read only property. + *

    + *

    + * You may also put use the Transient annotation with the Formula annotation. + * The effect of the Transient annotation in this case is that the formula will + * NOT be included in queries by default - you have to explicitly include + * it via {@link Query#select(String)} or {@link Query#join(String, String)}. + * You may want to do this if the Formula is relatively expensive and only want + * it included in the query when you explicitly state it. + *

    + * + *
    + * // On the Order "master" bean
    + * // ... a formula using the Order details
    + * // ... sum(order_qty*unit_price)
    + * @Transient
    + * @Formula(select = "_b${ta}.total_amount", join = "join (select order_id, sum(order_qty*unit_price) as total_amount from o_order_detail group by order_id) as _b${ta} on _b${ta}.order_id = ${ta}.id")
    + * Double totalAmount;
    + * 
    + * 
    + *

    + * As the totalAmount formula is also Transient it is not included by default in + * queries - it needs to be explicitly included. + *

    + * + *
    + * // find by Id
    + * Order o1 = Ebean.find(Order.class)
    + *     .select("id, totalAmount")
    + *     .setId(1).findUnique();
    + * 
    + * // find list ... using totalAmount in the where clause
    + * List<Order> list = Ebean.find(Order.class)
    + *     .select("id, totalAmount")
    + *     .where()
    + *     .eq("status", Order.Status.NEW)
    + *     .gt("totalAmount", 10)
    + *     .findList();
    + * 
    + * // as a join from customer
    + * List<Customer> l0 = Ebean.find(Customer.class)
    + *     .select("id, name")
    + *     .join("orders", "status, totalAmount")
    + *     .where()
    + *     .gt("id", 0)
    + *     .gt("orders.totalAmount", 10)
    + *     .findList();
    + * 
    + * 
    + */ +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Formula { + + /** + * The SQL to be used in the SELECT part of the SQL to populate a property. + */ + String select(); + + /** + * OPTIONAL - the SQL to be used in the JOIN part of the SQL to support the + * formula. + *

    + * This is commonly used to join a 'dynamic view' to support aggregation such + * as count, sum etc. + *

    + *

    + * The join string should start with either "left outer join" or "join". + *

    + * + *

    + * You will almost certainly use the "${ta}" as a place holder for the table + * alias of the table you are joining back to (the "base table" of the entity + * bean). + *

    + *

    + * The example below is used to support a total count of topics created by a + * user. + *

    + * + *
    +   * join (select user_id, count(*) as topic_count from f_topic group by user_id) as _tc on _tc.user_id = ${ta}.id
    +   * 
    + */ + String join() default ""; + +}; diff --git a/src/main/java/com/avaje/ebean/annotation/NamedUpdate.java b/src/main/java/com/avaje/ebean/annotation/NamedUpdate.java new file mode 100644 index 000000000..82ede2e27 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/NamedUpdate.java @@ -0,0 +1,37 @@ +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; + +/** + * An Update statement for a particular entity bean type. + *

    + * The update can either be a sql insert,update or delete statement with tables + * and columns etc or the equivalent statement but with table names and columns + * expressed as bean types and bean properties. + *

    + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface NamedUpdate { + + /** + * The name of the update. + */ + String name(); + + /** + * The insert, update or delete statement. + */ + String update(); + + /** + * Set this to false if you do not want the cache to be notified. If true the + * cache will invalidate appropriate objects from the cache (after a + * successful transaction commit). + */ + boolean notifyCache() default true; + +}; diff --git a/src/main/java/com/avaje/ebean/annotation/NamedUpdates.java b/src/main/java/com/avaje/ebean/annotation/NamedUpdates.java new file mode 100644 index 000000000..b753d8d5a --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/NamedUpdates.java @@ -0,0 +1,20 @@ +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; + +/** + * Holds an array of named Update statements for a particular entity bean type. + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface NamedUpdates { + + /** + * An array of named updates. + */ + NamedUpdate[] value(); + +}; diff --git a/src/main/java/com/avaje/ebean/annotation/PrivateOwned.java b/src/main/java/com/avaje/ebean/annotation/PrivateOwned.java new file mode 100644 index 000000000..b5406d871 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/PrivateOwned.java @@ -0,0 +1,34 @@ +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; + +/** + * Specify that the elements of a OneToMany are private owned. + *

    + * This means that if they are removed from the List/Set/Map they will be + * deleted when their parent object is saved. + *

    + *

    + * This could also be described as deleting orphans - in that beans removed from + * the List/Set/Map will be deleted automatically when the parent bean is saved. + * They are considered 'orphans' when they have been removed from the collection + * in that they are no longer associated/linked to their parent bean. + *

    + */ +@Target({ ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface PrivateOwned { + + /** + * Set this to false if you don't want cascade REMOVE on this relationship. + *

    + * That is, by default PrivateOwned implicitly adds a cascade REMOVE to the + * relationship and if you don't want that you need to set this to false. + *

    + */ + boolean cascadeRemove() default true; + +}; diff --git a/src/main/java/com/avaje/ebean/annotation/Sql.java b/src/main/java/com/avaje/ebean/annotation/Sql.java new file mode 100644 index 000000000..eecbc2eb4 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/Sql.java @@ -0,0 +1,24 @@ +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; + +/** + * Specify explicit sql for multiple select statements. Need to use this if you + * have more than one SqlSelect for a given bean. + *

    + * FUTURE: Support explicit sql for SqlInsert, SqlUpdate and SqlDelete. + *

    + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Sql { + + /** + * The sql select statements. + */ + SqlSelect[] select() default { @SqlSelect }; + +}; diff --git a/src/main/java/com/avaje/ebean/annotation/SqlSelect.java b/src/main/java/com/avaje/ebean/annotation/SqlSelect.java new file mode 100644 index 000000000..04acb88a9 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/SqlSelect.java @@ -0,0 +1,298 @@ +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; + +import com.avaje.ebean.Query; + +/** + * Specify an explicit sql select statement to use for querying an entity bean. + *

    + * The reason for using explicit sql is that you want better control over the + * exact sql or sql that Ebean does not generate for you (such as group by, + * union, intersection, window functions, recursive queries). + *

    + *

    + * An example of two sql select queries deployed on the ReportTopic entity bean. + * The first one has no name specified so it becomes the default query. The + * second query extends the first adding a where clause with a named parameter. + *

    + * + *
    + * ...
    + * @Entity
    + *   @Sql(select = {
    + *     @SqlSelect(query = 
    + *       "select t.id, t.title, count(p.id) as score "+
    + *       "from f_topic t "+
    + *       "join f_topic_post p on p.topic_id = t.id "+
    + *       "group by t.id, t.title"),
    + *     @SqlSelect(
    + *       name = "with.title",
    + *       extend = "default",
    + *       debug = true,
    + *       where = "title like :likeTitle")
    + *  })
    + *  public class ReportTopic
    + *    @Id Integer id;
    + *    String title;
    + *    Double score;
    + *    ...
    + * 
    + * + *

    + * An example using the first "default" query. + *

    + * + *
    + * 
    + * List<ReportTopic> list =
    + *     Ebean.find(ReportTopic.class)
    + *         .having().gt("score", 0)
    + *         .findList();
    + * 
    + * 
    + * + *

    + * The resulting sql, note the having clause has been added. + *

    + * + *
    + * select t.id, t.title, count(p.id) as score 
    + * from f_topic t join f_topic_post p on p.topic_id = t.id 
    + * group by t.id, t.title  
    + * having count(p.id) > ?
    + * 
    + * + *

    + * An example using the second query. Note the named parameter "likeTitle" must + * be set. + *

    + * + *
    + * List<ReportTopic> list =
    + *     Ebean.find(ReportTopic.class, "with.title")
    + *         .set("likeTitle", "a%")
    + *         .findList();
    + * 
    + * + *

    + * Ebean tries to parse the sql in the query to determine 4 things + *

  • Location for inserting WHERE expressions (if required)
  • + *
  • Location for inserting HAVING expressions (if required)
  • + *
  • Mapping of columns to bean properties
  • + *
  • The order by clause
  • + *

    + *

    + * If Ebean is unable to parse out this information (perhaps because the sql + * contains multiple select from keywords etc) then you need to manually specify + * it. + *

    + *

    + * Insert ${where} or ${andWhere} into the location where Ebean can insert any + * expressions added to the where clause. Use ${andWhere} if the sql already has + * the WHERE keyword and Ebean will instead start with a AND keyword. + *

    + *

    + * Insert ${having} or ${andHaving} into the location where Ebean can insert any + * expressions added to the having clause. Use ${andHaving} if the sql already + * has a HAVING keyword and Ebean will instead start with a AND keyword. + *

    + *

    + * Use the columnMapping property if Ebean is unable to determine the columns + * and map them to bean properties. + *

    + *

    + * Example with ${andWhere} & ${having}. + *

    + * + *
    + *    @SqlSelect(
    + *          name = "explicit.where",
    + *          query = 
    + *              "select t.id, t.title, count(p.id) as score "+
    + *              "from f_topic t, f_topic_post p "+
    + *              "where p.topic_id = t.id ${andWhere} "+
    + *              "group by t.id, t.title ${having}"),
    + * 
    + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Deprecated +public @interface SqlSelect { + + /** + * The name of the query. If left blank this is assumed to be the default + * query for this bean type. + *

    + * This will default to "default" and in that case becomes the default query + * used for the bean. + *

    + */ + String name() default "default"; + + /** + * The tableAlias used when adding where expressions to the query. + */ + String tableAlias() default ""; + + /** + * The sql select statement. + *

    + * If this query extends another then this string is appended to the + * parent query string. Often when using extend you will leave the + * query part blank and just specify a where and/or having clauses. + *

    + *

    + * This sql CAN NOT contain named parameters. You have to put these + * in the separate where and/or having sections. + *

    + *

    + * Ebean automatically tries to determine the location in the sql string for + * putting in additional where or having clauses. If Ebean is unable to + * successfully determine this then you have to explicitly specify these + * locations by including + * ${where} or ${andWhere} and ${having} or ${andHaving} in the sql. + *

    + *

    + * ${where} location of where clause (and will add WHERE ... )
    + * Use this when there is no where clause in the sql. If expressions are added + * to the where clause Ebean will put them in at this location starting with + * the WHERE keyword. + *

    + *

    + * ${andWhere}
    + * Use this instead of ${where} if there IS an existing where clause in the + * sql. Ebean will add the expressions starting with the AND keyword. + *

    + * ${having} location of having clause (and will add HAVING... )
    + *

    + *

    + * ${andHaving}
    + * Use this instead of ${having} when there IS an existing HAVING clause. + * Ebean will add the expressions starting with the AND keyword. + *

    + *

    + * You can include one of ${where} OR ${andWhere} but not both. + *

    + *

    + * You can include one of ${having} OR ${andHaving} but not both. + *

    + */ + String query() default ""; + + /** + * Specify the name of a sql-select query that this one 'extends'. + *

    + * When a query is extended the sql query contents are appended together. The + * where and having clauses are NOT appended but overridden. + *

    + */ + String extend() default ""; + + /** + * Specify a where clause typically containing named parameters. + *

    + * If a where clause is specified with named parameters then they will need to + * be set on the query via {@link Query#setParameter(String, Object)}. + *

    + *

    + * In the example below the query specifies a where clause that includes a + * named parameter "likeTitle". + *

    + * + *
    +   * ...
    +   * @Entity
    +   * @Sql(select = {
    +   *  ...
    +   *  @SqlSelect(
    +   *  name = "with.title",
    +   *  extend = "default",
    +   *  debug = true,
    +   *  where = "title like :likeTitle")
    +   *  })
    +   *  public class ReportTopic
    +   *  ...
    +   * 
    + * + *

    + * Example use of the above named query. + *

    + * + *
    +   * 
    +   * Query<ReportTopic> query0 = Ebean.createQuery(ReportTopic.class, "with.title");
    +   * 
    +   * query0.set("likeTitle", "Bana%");
    +   * 
    +   * List<ReportTopic> list0 = query0.findList();
    +   * 
    + * + */ + String where() default ""; + + /** + * Specify a having clause typically containing named parameters. + *

    + * If a having clause is specified with named parameters then they will need + * to be set on the query via {@link Query#setParameter(String, Object)}. + *

    + */ + String having() default ""; + + /** + * (Optional) Explicitly specify column to property mapping. + *

    + * This is required when Ebean is unable to parse the sql. This could occur if + * the sql contains multiple select keywords etc. + *

    + *

    + * Specify the columns and property names they map to in the format. + *

    + * + *
    +   *  column1 propertyName1, column2 propertyName2, ...
    +   * 
    + * + *

    + * Optionally put a AS keyword between the column and property. + *

    + * + *
    +   *   // the AS keyword is optional
    +   *  column1 AS propertyName1, column2 propertyName2, ...
    +   * 
    + * + *

    + * column should contain the table alias if there is one + *

    + *

    + * propertyName should match the property name. + *

    + * + *

    + * Example mapping 5 columns to properties. + *

    + * + *
    +   * columnMapping="t.id, t.bug_body description, t.bug_title as title, count(p.id) as scoreValue",
    +   * 
    + * + *

    + * Without this set Ebean will parse the sql looking for the select clause and + * try to map the columns to property names. It is expected that Ebean will + * not be able to successfully parse some sql and for those cases you should + * specify the column to property mapping explicitly. + *

    + * + */ + String columnMapping() default ""; + + /** + * Set this to true to have debug output when Ebean parses the sql-select. + */ + boolean debug() default false; +}; diff --git a/src/main/java/com/avaje/ebean/annotation/Transactional.java b/src/main/java/com/avaje/ebean/annotation/Transactional.java new file mode 100644 index 000000000..11439111a --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/Transactional.java @@ -0,0 +1,118 @@ +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; + +import com.avaje.ebean.TxIsolation; +import com.avaje.ebean.TxType; + +/** + * Specify transaction scoping for a method. + *

    + * This is only supported if "Enhancement" is used via javaagent, ANT + * task or IDE enhancement plugin etc. + *

    + *

    + * Note: Currently there are 3 known annotations that perform this role. + *

      + *
    • EJB's javax.ejb.TransactionAttribute
    • + *
    • Spring's org.springframework.transaction.annotation.Transactional
    • + *
    • and this one, Ebean's own com.avaje.ebean.annotation.Transactional
    • + *
    + * Spring created their one because the EJB annotation does not support features + * such as isolation level and specifying rollbackOn, noRollbackOn exceptions. + * This one exists for Ebean because I agree that the standard one is + * insufficient and don't want to include a dependency on Spring. + *

    + *

    + * The default behaviour of EJB (and hence Spring) is to NOT ROLLBACK on checked + * exceptions. I find this very counter-intuitive. Ebean will provide a property + * to set the default behaviour to rollback on any exception and optionally + * change the setting to be consistent with EJB/Spring if people wish to do so. + *

    + * + *
    + * 
    + *  // a normal class
    + * public class MySimpleUserService {
    + * 
    + *  // this method is transactional automatically handling 
    + *  // transaction begin, commit and rollback etc
    + *  @Transactional
    + *  public void runInTrans() throws IOException {
    + * 
    + *    // tasks performed within the transaction
    + *    ...
    + *    // find some objects
    + *    Customer cust = Ebean.find(Customer.class, 1);
    + *    
    + *    Order order = ...;
    + *    ...
    + *    // save some objects
    + *    Ebean.save(customer);
    + *    Ebean.save(order);
    + *  }
    + * 
    + * + *

    + * During development and testing you can set a debug level which will log the + * transaction begin, commit and rollback events so that you can easily confirm + * it is behaving as you would expect. + *

    + * + *
    + *  ## in ebean.properties file
    + *  
    + *  ## Log transaction begins and ends etc
    + *  ## (0=NoLogging 1=minimal ... 9=logAll)
    + *  ebean.debug.transaction=3
    + * 
    + * 
    + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Transactional { + + /** + * The type of transaction scoping. Defaults to REQUIRED. + */ + TxType type() default TxType.REQUIRED; + + /** + * The transaction isolation level this transaction should have. + *

    + * This will only be used if this scope creates the transaction. If the + * transaction has already started then this will currently be ignored (you + * could argue that it should throw an exception). + *

    + */ + TxIsolation isolation() default TxIsolation.DEFAULT; + + /** + * Set this to true if the transaction should be only contain queries. + */ + boolean readOnly() default false; + + /** + * The name of the server that you want the transaction to be created from. + *

    + * If left blank the 'default' server is used. + *

    + */ + String serverName() default ""; + + // int timeout() default 0; + + /** + * The throwable's that will explicitly cause a rollback to occur. + */ + Class[] rollbackFor() default {}; + + /** + * The throwable's that will explicitly NOT cause a rollback to occur. + */ + Class[] noRollbackFor() default {}; + +}; diff --git a/src/main/java/com/avaje/ebean/annotation/UpdateMode.java b/src/main/java/com/avaje/ebean/annotation/UpdateMode.java new file mode 100644 index 000000000..847fff403 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/UpdateMode.java @@ -0,0 +1,34 @@ +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; + +/** + * Specify the update mode for the specific entity type. + *

    + * Control whether all 'loaded' properties are included in an Update or whether + * just properties that have changed will be included in the update. + *

    + *

    + * Note that the default can be set via ebean.properties. + *

    + * + *
    + * ## Set to update all loaded properties
    + * ebean.updateChangesOnly=false
    + * 
    + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface UpdateMode { + + /** + * Set to false if you want to include all the 'loaded' properties in the + * update. Otherwise, just the properties that have changed will be included + * in the update. + */ + boolean updateChangesOnly() default true; + +}; diff --git a/src/main/java/com/avaje/ebean/annotation/UpdatedTimestamp.java b/src/main/java/com/avaje/ebean/annotation/UpdatedTimestamp.java new file mode 100644 index 000000000..f5c8d89a7 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/UpdatedTimestamp.java @@ -0,0 +1,16 @@ +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; + +/** + * For a timestamp property that is set to the datetime when the entity was last + * updated. + */ +@Target({ ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface UpdatedTimestamp { + +}; diff --git a/src/main/java/com/avaje/ebean/annotation/Where.java b/src/main/java/com/avaje/ebean/annotation/Where.java new file mode 100644 index 000000000..138ef33c2 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/Where.java @@ -0,0 +1,51 @@ +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; + +/** + * Add an Literal to add to the where clause when a many property (List, Set or + * Map) is loaded or refreshed. + * + *
    + * // on a OneToMany property...
    + * 
    + * @OneToMany
    + * @Where(clause = "deleted='y'")
    + * List<Topic> topics;
    + * 
    + * + *

    + * Note that you can include "${ta}" as a place holder for the table alias if + * you need to include the table alias in the clause. + *

    + * + *
    + * // ... including the ${ta} table alias placeholder...
    + * 
    + * @OneToMany
    + * @Where(clause = "${ta}.deleted='y'")
    + * List<Topic> topics;
    + * 
    + * + *

    + * This will be added to the where clause when lazy loading the OneToMany + * property or when there is a join to that OneToMany property. + *

    + */ +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Where { + + /** + * The clause added to the lazy load query. + *

    + * Note that you can include "${ta}" as a place holder for the table alias if + * you need to include the table alias in the clause. + *

    + */ + String clause(); + +}; diff --git a/src/main/java/com/avaje/ebean/annotation/package.html b/src/main/java/com/avaje/ebean/annotation/package.html new file mode 100644 index 000000000..c39eed7aa --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/package.html @@ -0,0 +1,14 @@ + + + + Extra deployment annotations + + +Extra deployment annotations + +

    +Extra deployment annotations for entity beans. +

    + + + \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/bean/BeanCollection.java b/src/main/java/com/avaje/ebean/bean/BeanCollection.java new file mode 100644 index 000000000..a8955b341 --- /dev/null +++ b/src/main/java/com/avaje/ebean/bean/BeanCollection.java @@ -0,0 +1,243 @@ +package com.avaje.ebean.bean; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import com.avaje.ebean.ExpressionList; +import com.avaje.ebean.Query; + +/** + * Lazy loading capable Maps, Lists and Sets. + *

    + * This also includes the ability to listen for additions and removals to or + * from the Map Set or List. The purpose of gathering the additions and removals + * is to support persisting ManyToMany objects. The additions and removals + * become inserts and deletes from the intersection table. + *

    + *

    + * Technically this is NOT an extension of + * java.util.Collection. The reason being that java.util.Map is not a + * Collection. I realise this makes this name confusing so I apologise for that. + *

    + */ +public interface BeanCollection extends Serializable { + + public enum ModifyListenMode { + /** The common mode */ + NONE, + /** Mode used for PrivateOwned */ + REMOVALS, + /** Mode used for ManyToMany relationships */ + ALL + } + + /** + * Return the bean that owns this collection. + */ + public Object getOwnerBean(); + + /** + * Return the bean property name this collection represents. + */ + public String getPropertyName(); + + /** + * Return the index position of this collection in the lazy/query loader. + *

    + * Used for batch loading of collections. + *

    + */ + public int getLoaderIndex(); + + /** + * Check after the lazy load that the underlying collection is not null + * (handle case where join to many not outer). + *

    + * That is, if the collection was not loaded due to filterMany predicates etc + * then make sure the collection is set to empty. + *

    + */ + public boolean checkEmptyLazyLoad(); + + /** + * Return the filter (if any) that was used in building this collection. + *

    + * This is so that the filter can be applied on refresh. + *

    + */ + public ExpressionList getFilterMany(); + + /** + * Set the filter that was used in building this collection. + */ + public void setFilterMany(ExpressionList filterMany); + + /** + * Set when this collection is being loaded via a background thread. + *

    + * Refer to {@link Query#setBackgroundFetchAfter(int)} + *

    + */ + public void setBackgroundFetch(Future future); + + /** + * Wait for the fetch to complete with a given timeout. + *

    + * Refer to {@link Query#setBackgroundFetchAfter(int)} + *

    + */ + public void backgroundFetchWait(long wait, TimeUnit timeUnit); + + /** + * Wait for the fetch to complete. + *

    + * Refer to {@link Query#setBackgroundFetchAfter(int)} + *

    + */ + public void backgroundFetchWait(); + + /** + * Set a listener to be notified when the BeanCollection is first touched. + */ + public void setBeanCollectionTouched(BeanCollectionTouched notify); + + /** + * Set the loader that will be used to lazy/query load this collection. + */ + public void setLoader(int beanLoaderIndex, BeanCollectionLoader beanLoader); + + /** + * Set to true if you want the BeanCollection to be treated as read only. This + * means no elements can be added or removed etc. + */ + public void setReadOnly(boolean readOnly); + + /** + * Return true if the collection should be treated as readOnly and no elements + * can be added or removed etc. + */ + public boolean isReadOnly(); + + /** + * Add the bean to the collection. + *

    + * This is disallowed for BeanMap. + *

    + */ + public void internalAdd(Object bean); + + /** + * Returns the underlying List Set or Map object. + */ + public Object getActualCollection(); + + /** + * Return the number of elements in the List Set or Map. + */ + public int size(); + + /** + * Return true if the List Set or Map is empty. + */ + public boolean isEmpty(); + + /** + * Returns the underlying details as an iterator. + *

    + * Note that for maps this returns the entrySet as we need the keys of the + * map. + *

    + */ + public Collection getActualDetails(); + + /** + * Set to true if maxRows was hit and there are actually more rows available. + *

    + * Can be used by client code that is paging through results using + * setFirstRow() setMaxRows(). If this returns true then the client can + * display a 'next' button etc. + *

    + */ + public boolean hasMoreRows(); + + /** + * Set to true when maxRows is hit but there are actually more rows available. + * This is set so that client code knows that there is more data available. + */ + public void setHasMoreRows(boolean hasMoreRows); + + /** + * Returns true if the fetch has finished. False if the fetch is continuing in + * a background thread. + */ + public boolean isFinishedFetch(); + + /** + * Set to true when a fetch has finished. Used when a fetch continues in the + * background. + */ + public void setFinishedFetch(boolean finishedFetch); + + /** + * return true if there are real rows held. Return false is this is using + * Deferred fetch to lazy load the rows and the rows have not yet been + * fetched. + */ + public boolean isPopulated(); + + /** + * Return true if this is a reference (lazy loading) bean collection. This is + * the same as !isPopulated(); + */ + public boolean isReference(); + + /** + * Set modify listening on or off. This is used to keep track of objects that + * have been added to or removed from the list set or map. + *

    + * This is required only for ManyToMany collections. The additions and + * deletions are used to insert or delete entries from the intersection table. + * Otherwise modifyListening is false. + *

    + */ + public void setModifyListening(ModifyListenMode modifyListenMode); + + /** + * Add an object to the additions list. + *

    + * This will potentially end up as an insert into a intersection table for a + * ManyToMany. + *

    + */ + public void modifyAddition(E bean); + + /** + * Add an object to the deletions list. + *

    + * This will potentially end up as an delete from an intersection table for a + * ManyToMany. + *

    + */ + public void modifyRemoval(Object bean); + + /** + * Return the list of objects added to the list set or map. These will used to + * insert rows into the intersection table of a ManyToMany. + */ + public Set getModifyAdditions(); + + /** + * Return the list of objects removed from the list set or map. These will + * used to delete rows from the intersection table of a ManyToMany. + */ + public Set getModifyRemovals(); + + /** + * Reset the set of additions and deletions. This is called after the + * additions and removals have been processed. + */ + public void modifyReset(); +} diff --git a/src/main/java/com/avaje/ebean/bean/BeanCollectionAdd.java b/src/main/java/com/avaje/ebean/bean/BeanCollectionAdd.java new file mode 100644 index 000000000..8bc754166 --- /dev/null +++ b/src/main/java/com/avaje/ebean/bean/BeanCollectionAdd.java @@ -0,0 +1,16 @@ +package com.avaje.ebean.bean; + +/** + * Interface to define the addition of a bean to the underlying collection. + *

    + * For maps this takes into account the map key. For List and Set this simply + * adds the bean to the underlying list or set. + *

    + */ +public interface BeanCollectionAdd { + + /** + * Add a loaded bean to the collection. + */ + public void addBean(Object bean); +} diff --git a/src/main/java/com/avaje/ebean/bean/BeanCollectionLoader.java b/src/main/java/com/avaje/ebean/bean/BeanCollectionLoader.java new file mode 100644 index 000000000..8b8781d8a --- /dev/null +++ b/src/main/java/com/avaje/ebean/bean/BeanCollectionLoader.java @@ -0,0 +1,21 @@ +package com.avaje.ebean.bean; + +/** + * Loads a entity bean collection. + *

    + * Typically invokes lazy loading for a single or batch of collections. + *

    + */ +public interface BeanCollectionLoader { + + /** + * Return the name of the associated EbeanServer. + */ + public String getName(); + + /** + * Invoke the lazy loading for this bean collection. + */ + public void loadMany(BeanCollection collection, boolean onlyIds); + +} diff --git a/src/main/java/com/avaje/ebean/bean/BeanCollectionTouched.java b/src/main/java/com/avaje/ebean/bean/BeanCollectionTouched.java new file mode 100644 index 000000000..2962e2206 --- /dev/null +++ b/src/main/java/com/avaje/ebean/bean/BeanCollectionTouched.java @@ -0,0 +1,20 @@ +package com.avaje.ebean.bean; + +/** + * Used to specify a listener to be notified when a BeanCollection is first + * used. + *

    + * To use this you can set a BeanCollectionTouched onto a BeanCollection before + * it has been used. When the BeanCollection is first used by the client code + * then the BeanCollectionTouched is notified. It can only be notified once. + *

    + * + * @author rbygrave + */ +public interface BeanCollectionTouched { + + /** + * Notify the listener that the bean collection has been used. + */ + public void notifyTouched(BeanCollection c); +} diff --git a/src/main/java/com/avaje/ebean/bean/BeanLoader.java b/src/main/java/com/avaje/ebean/bean/BeanLoader.java new file mode 100644 index 000000000..6c169efa7 --- /dev/null +++ b/src/main/java/com/avaje/ebean/bean/BeanLoader.java @@ -0,0 +1,21 @@ +package com.avaje.ebean.bean; + +/** + * Loads a entity bean. + *

    + * Typically invokes lazy loading for a single or batch of entity beans. + *

    + */ +public interface BeanLoader { + + /** + * Return the name of the associated EbeanServer. + */ + public String getName(); + + /** + * Invoke the lazy loading for this bean. + */ + public void loadBean(EntityBeanIntercept ebi); + +} diff --git a/src/main/java/com/avaje/ebean/bean/CallStack.java b/src/main/java/com/avaje/ebean/bean/CallStack.java new file mode 100644 index 000000000..66f7a937f --- /dev/null +++ b/src/main/java/com/avaje/ebean/bean/CallStack.java @@ -0,0 +1,98 @@ +package com.avaje.ebean.bean; + +import java.io.Serializable; + +/** + * Represent the call stack (stack trace elements). + *

    + * Used with a query to identify a CallStackQuery for AutoFetch automatic query + * tuning. + *

    + *

    + * This is used so that a single query called from different methods can be + * tuned for each different call stack. + *

    + *

    + * Note the call stack is trimmed to remove the common ebean internal elements. + *

    + */ +public final class CallStack implements Serializable { + + private static final long serialVersionUID = -8590644046907438579L; + + private final String zeroHash; + private final String pathHash; + + private final StackTraceElement[] callStack; + + public CallStack(StackTraceElement[] callStack) { + this.callStack = callStack; + this.zeroHash = enc(callStack[0].hashCode()); + int hc = 0; + for (int i = 1; i < callStack.length; i++) { + hc = 31 * hc + callStack[i].hashCode(); + } + this.pathHash = enc(hc); + } + + /** + * Return the first element of the call stack. + */ + public StackTraceElement getFirstStackTraceElement() { + return callStack[0]; + } + + /** + * Return the call stack. + */ + public StackTraceElement[] getCallStack() { + return callStack; + } + + /** + * Return the hash for the first stack element. + */ + public String getZeroHash() { + return zeroHash; + } + + /** + * Return the hash for the stack elements (excluding first stack element). + */ + public String getPathHash() { + return pathHash; + } + + public String toString() { + return zeroHash + ":" + pathHash + ":" + callStack[0]; + } + + public String getOriginKey(int queryHash) { + return zeroHash + "." + enc(queryHash) + "." + pathHash; + } + + private static final int radix = 1 << 6; + private static final int mask = radix - 1; + + /** + * Convert the integer to unsigned base 64. + */ + public static String enc(int i) { + char[] buf = new char[32]; + int charPos = 32; + do { + buf[--charPos] = intToBase64[i & mask]; + i >>>= 6; + } while (i != 0); + + return new String(buf, charPos, (32 - charPos)); + } + + private static final char intToBase64[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_' + }; +} diff --git a/src/main/java/com/avaje/ebean/bean/EntityBean.java b/src/main/java/com/avaje/ebean/bean/EntityBean.java new file mode 100644 index 000000000..701691126 --- /dev/null +++ b/src/main/java/com/avaje/ebean/bean/EntityBean.java @@ -0,0 +1,127 @@ +package com.avaje.ebean.bean; + +import java.beans.PropertyChangeListener; +import java.io.Serializable; + +/** + * Bean that is aware of EntityBeanIntercept. + *

    + * This interface and implementation of these methods is added to Entity Beans + * via instrumentation. These methods have a funny _ebean_ prefix to avoid any + * clash with normal methods these beans would have. These methods are not for + * general application consumption. + *

    + */ +public interface EntityBean extends Serializable { + + /** + * Return the enhancement marker value. + *

    + * This is the class name of the enhanced class and used to check that all + * entity classes are enhanced (specifically not just a super class). + *

    + */ + public String _ebean_getMarker(); + + /** + * Create and return a new entity bean instance. + */ + public Object _ebean_newInstance(); + + /** + * Add a PropertyChangeListener to this bean. + */ + public void addPropertyChangeListener(PropertyChangeListener listener); + + /** + * Remove a PropertyChangeListener from this bean. + */ + public void removePropertyChangeListener(PropertyChangeListener listener); + + /** + * Generated method that sets the loaded state on all the embedded beans on + * this entity bean by using EntityBeanIntercept.setEmbeddedLoaded(Object o); + */ + public void _ebean_setEmbeddedLoaded(); + + /** + * Return true if any embedded beans are new or dirty. + */ + public boolean _ebean_isEmbeddedNewOrDirty(); + + /** + * Return the intercept for this object. + */ + public EntityBeanIntercept _ebean_getIntercept(); + + /** + * Similar to _ebean_getIntercept() except it checks to see if the intercept + * field is null and will create it if required. + *

    + * This is really only required when transientInternalFields=true as an + * enhancement option. In this case the intercept field is transient and will + * be null after a bean has been deserialised. + *

    + *

    + * This transientInternalFields=true option was to support some serialization + * frameworks that can't take into account our ebean fields. + *

    + */ + public EntityBeanIntercept _ebean_intercept(); + + /** + * Create a copy of this entity bean. + *

    + * This occurs when a bean is changed. The copy represents the bean as it was + * initially (oldValues) before any changes where made. This is used for + * optimistic concurrency control. + *

    + */ + public Object _ebean_createCopy(); + + /** + * Return the fields in their index order. + */ + public String[] _ebean_getFieldNames(); + + /** + * Set the value of a field of an entity bean of this type. + *

    + * Note that using this method bypasses any interception that otherwise occurs + * on entity beans. That means lazy loading and oldValues creation. + *

    + * + * @param fieldIndex + * the index of the field + * @param entityBean + * the entityBean of this type to modify + * @param value + * the value to set + */ + public void _ebean_setField(int fieldIndex, Object entityBean, Object value); + + /** + * Set the field value with interception. + */ + public void _ebean_setFieldIntercept(int fieldIndex, Object entityBean, Object value); + + /** + * Return the value of a field from an entity bean of this type. + *

    + * Note that using this method bypasses any interception that otherwise occurs + * on entity beans. That means lazy loading. + *

    + * + * @param fieldIndex + * the index of the field + * @param entityBean + * the entityBean to get the value from + */ + public Object _ebean_getField(int fieldIndex, Object entityBean); + + /** + * Return the field value with interception. + */ + public Object _ebean_getFieldIntercept(int fieldIndex, Object entityBean); + +} diff --git a/src/main/java/com/avaje/ebean/bean/EntityBeanIntercept.java b/src/main/java/com/avaje/ebean/bean/EntityBeanIntercept.java new file mode 100644 index 000000000..9a62aeeb0 --- /dev/null +++ b/src/main/java/com/avaje/ebean/bean/EntityBeanIntercept.java @@ -0,0 +1,926 @@ +package com.avaje.ebean.bean; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.io.ObjectStreamException; +import java.io.Serializable; +import java.math.BigDecimal; +import java.net.URL; +import java.util.HashSet; +import java.util.Set; + +import javax.persistence.EntityNotFoundException; +import javax.persistence.PersistenceException; + +import com.avaje.ebean.Ebean; + +/** + * This is the object added to every entity bean using byte code enhancement. + *

    + * This provides the mechanisms to support deferred fetching of reference beans + * and oldValues generation for concurrency checking. + *

    + */ +public final class EntityBeanIntercept implements Serializable { + + private static final long serialVersionUID = -3664031775464862648L; + + private transient NodeUsageCollector nodeUsageCollector; + + private transient PropertyChangeSupport pcs; + + private transient PersistenceContext persistenceContext; + + private transient BeanLoader beanLoader; + + private int beanLoaderIndex; + + private String ebeanServerName; + + /** + * The actual entity bean that 'owns' this intercept. + */ + private EntityBean owner; + + /** + * The parent bean by relationship (1-1 or 1-M). + */ + private Object parentBean; + + /** + * true if the bean properties have been loaded. false if it is a reference + * bean (will lazy load etc). + */ + private volatile boolean loaded; + + /** + * Flag set to disable lazy loading - typically for SQL "report" type entity + * beans. + */ + private boolean disableLazyLoad; + + /** + * Flag set when lazy loading failed due to the underlying bean being deleted + * in the DB. + */ + private boolean lazyLoadFailure; + + /** + * Set true when loaded or reference. Used to bypass interception when created + * by user code. + */ + private boolean intercepting; + + /** + * The state of the Bean (DEFAULT,UDPATE,READONLY,SHARED). + */ + private boolean readOnly; + + /** + * The bean as it was before it was modified. Null if no non-transient setters + * have been called. + */ + private Object oldValues; + + /** + * Used when a bean is partially filled. + */ + private volatile Set loadedProps; + + /** + * Set of changed properties. + */ + private HashSet changedProps; + + private String lazyLoadProperty; + + /** + * Create a intercept with a given entity. + *

    + * Refer to agent ProxyConstructor. + *

    + */ + public EntityBeanIntercept(Object owner) { + this.owner = (EntityBean) owner; + } + + /** + * Copy the internal state of the intercept to another intercept. + */ + public void copyStateTo(EntityBeanIntercept dest) { + dest.loadedProps = loadedProps; + dest.ebeanServerName = ebeanServerName; + + if (loaded) { + dest.setLoaded(); + } + } + + /** + * Return the 'owning' entity bean. + */ + public EntityBean getOwner() { + return owner; + } + + public String toString() { + if (!loaded) { + return "Reference..."; + } + return "OldValues: " + oldValues; + } + + /** + * Return the persistenceContext. + */ + public PersistenceContext getPersistenceContext() { + return persistenceContext; + } + + /** + * Set the persistenceContext. + */ + public void setPersistenceContext(PersistenceContext persistenceContext) { + this.persistenceContext = persistenceContext; + } + + /** + * Add a property change listener for this entity bean. + */ + public void addPropertyChangeListener(PropertyChangeListener listener) { + if (pcs == null) { + pcs = new PropertyChangeSupport(owner); + } + pcs.addPropertyChangeListener(listener); + } + + /** + * Add a property change listener for this entity bean for a specific + * property. + */ + public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { + if (pcs == null) { + pcs = new PropertyChangeSupport(owner); + } + pcs.addPropertyChangeListener(propertyName, listener); + } + + /** + * Remove a property change listener for this entity bean. + */ + public void removePropertyChangeListener(PropertyChangeListener listener) { + if (pcs != null) { + pcs.removePropertyChangeListener(listener); + } + } + + /** + * Remove a property change listener for this entity bean for a specific + * property. + */ + public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { + if (pcs != null) { + pcs.removePropertyChangeListener(propertyName, listener); + } + } + + /** + * Turn on profile collection. + */ + public void setNodeUsageCollector(NodeUsageCollector usageCollector) { + this.nodeUsageCollector = usageCollector; + } + + /** + * Return the parent bean (by relationship). + */ + public Object getParentBean() { + return parentBean; + } + + /** + * Special case for a OneToOne, Set the parent bean (by relationship). This is + * the owner of a 1-1. + */ + public void setParentBean(Object parentBean) { + this.parentBean = parentBean; + } + + /** + * Return the index position for batch loading via BeanLoader. + */ + public int getBeanLoaderIndex() { + return beanLoaderIndex; + } + + /** + * Set Lazy Loading by ebeanServerName. + *

    + * This is for reference beans created by themselves. + *

    + */ + public void setBeanLoaderByServerName(String ebeanServerName) { + this.beanLoaderIndex = 0; + this.beanLoader = null; + this.ebeanServerName = ebeanServerName; + } + + /** + * Set the BeanLoader for general lazy loading. + */ + public void setBeanLoader(int index, BeanLoader beanLoader, PersistenceContext ctx) { + this.beanLoaderIndex = index; + this.beanLoader = beanLoader; + this.persistenceContext = ctx; + this.ebeanServerName = beanLoader.getName(); + } + + /** + * Return true if this bean has been directly modified (it has oldValues) or + * if any embedded beans are either new or dirty (and hence need saving). + */ + public boolean isDirty() { + if (oldValues != null) { + return true; + } + // need to check all the embedded beans + return owner._ebean_isEmbeddedNewOrDirty(); + } + + /** + * Return true if this entity bean is new and not yet saved. + */ + public boolean isNew() { + return !intercepting && !loaded; + } + + /** + * Return true if the entity bean is new or dirty (and should be saved). + */ + public boolean isNewOrDirty() { + return isNew() || isDirty(); + } + + /** + * Return true if the entity is a reference. + */ + public boolean isReference() { + return intercepting && !loaded; + } + + /** + * Set this as a reference object. + */ + public void setReference() { + this.loaded = false; + this.intercepting = true; + } + + /** + * Return the old values used for ConcurrencyMode.ALL. + */ + public Object getOldValues() { + return oldValues; + } + + /** + * Return true if the bean should be treated as readOnly. If a setter method + * is called when it is readOnly an Exception is thrown. + */ + public boolean isReadOnly() { + return readOnly; + } + + /** + * Set the readOnly status. If readOnly then calls to setter methods through + * an exception. + */ + public void setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + } + + /** + * Return true if the bean currently has interception on. + *

    + * With interception on the bean will invoke lazy loading and dirty checking. + *

    + */ + public boolean isIntercepting() { + return intercepting; + } + + /** + * Turn interception off or on. + *

    + * This is to support custom serialisation mechanisms that just read all the + * properties on the bean. + *

    + * + */ + public void setIntercepting(boolean intercepting) { + this.intercepting = intercepting; + } + + /** + * Return true if the entity has been loaded. + */ + public boolean isLoaded() { + return loaded; + } + + /** + * Set the loaded state to true. + *

    + * Calls to setter methods after the bean is loaded can result in 'Old Values' + * being created to support ConcurrencyMode.ALL + *

    + *

    + * Worth noting that this is also set after a insert/update. By doing so it + * 'resets' the bean for making further changes and saving again. + *

    + */ + public void setLoaded() { + this.loaded = true; + this.oldValues = null; + this.intercepting = true; + this.owner._ebean_setEmbeddedLoaded(); + this.lazyLoadProperty = null; + this.changedProps = null; + } + + /** + * When finished loading for lazy or refresh on an already partially populated + * bean. + */ + public void setLoadedLazy() { + this.loaded = true; + this.intercepting = true; + this.lazyLoadProperty = null; + } + + /** + * Mark this bean as having failed lazy loading due to the underlying row + * being deleted. + *

    + * We mark the bean this way rather than immediately fail as we might be batch + * lazy loading and this bean might not be used by the client code at all. + * Instead we will fail as soon as the client code tries to use this bean. + *

    + */ + public void setLazyLoadFailure() { + this.lazyLoadFailure = true; + } + + /** + * Return true if the bean is marked as having failed lazy loading. + */ + public boolean isLazyLoadFailure() { + return lazyLoadFailure; + } + + /** + * Return true if lazy loading is disabled. + */ + public boolean isDisableLazyLoad() { + return disableLazyLoad; + } + + /** + * Set true to turn off lazy loading. + *

    + * Typically used to disable lazy loading on SQL based report beans. + *

    + */ + public void setDisableLazyLoad(boolean disableLazyLoad) { + this.disableLazyLoad = disableLazyLoad; + } + + /** + * Set the loaded status for the embedded bean. + */ + public void setEmbeddedLoaded(Object embeddedBean) { + if (embeddedBean instanceof EntityBean) { + EntityBean eb = (EntityBean) embeddedBean; + eb._ebean_getIntercept().setLoaded(); + } + } + + /** + * Return true if the embedded bean is new or dirty and hence needs saving. + */ + public boolean isEmbeddedNewOrDirty(Object embeddedBean) { + + if (embeddedBean == null) { + // if it was previously set then the owning bean would + // have oldValues containing the previous embedded bean + return false; + } + if (embeddedBean instanceof EntityBean) { + return ((EntityBean) embeddedBean)._ebean_getIntercept().isNewOrDirty(); + + } else { + // non-enhanced so must assume it is new and needs to be saved + return true; + } + } + + /** + * Set the property names for a partially loaded bean. + * + * @param loadedPropertyNames + * the names of the loaded properties + */ + public void setLoadedProps(Set loadedPropertyNames) { + this.loadedProps = loadedPropertyNames; + } + + /** + * Return the set of property names for a partially loaded bean. + */ + public Set getLoadedProps() { + return loadedProps; + } + + /** + * Return the set of property names for changed properties. + */ + public Set getChangedProps() { + return changedProps; + } + + /** + * Return the property read or write that triggered the lazy load. + */ + public String getLazyLoadProperty() { + return lazyLoadProperty; + } + + /** + * Load the bean when it is a reference. + */ + protected void loadBean(String loadProperty) { + + synchronized (this) { + if (beanLoader == null) { + BeanLoader serverLoader = (BeanLoader) Ebean.getServer(ebeanServerName); + if (serverLoader == null) { + throw new PersistenceException("Server [" + ebeanServerName + "] was not found?"); + } + + // For stand alone reference bean or after deserialisation lazy load + // using the ebeanServer. Synchronise only on the bean. + loadBeanInternal(loadProperty, serverLoader); + return; + } + } + + synchronized (beanLoader) { + // Lazy loading using LoadBeanContext which supports batch loading + // Synchronise on the beanLoader (a 'node' of the LoadBeanContext 'tree') + loadBeanInternal(loadProperty, beanLoader); + } + } + + /** + * Invoke the lazy loading. This method is synchronised externally. + */ + private void loadBeanInternal(String loadProperty, BeanLoader loader) { + + if (loaded && (loadedProps == null || loadedProps.contains(loadProperty))) { + // race condition where multiple threads calling preGetter concurrently + return; + } + + if (disableLazyLoad) { + loaded = true; + return; + } + + if (lazyLoadFailure) { + // failed when batch lazy loaded by another bean in the batch + throw new EntityNotFoundException("Bean has been deleted - lazy loading failed"); + } + + if (lazyLoadProperty == null) { + + lazyLoadProperty = loadProperty; + + if (nodeUsageCollector != null) { + nodeUsageCollector.setLoadProperty(lazyLoadProperty); + } + + loader.loadBean(this); + + if (lazyLoadFailure) { + // failed when lazy loading this bean + throw new EntityNotFoundException("Bean has been deleted - lazy loading failed"); + } + + // bean should be loaded and intercepting now. setLoaded() has + // been called by the lazy loading mechanism + } + } + + /** + * Create a copy of the bean as it is now. This is the original or 'old + * values' prior to any modification. This is used to perform concurrency + * testing. + */ + protected void createOldValues() { + + oldValues = owner._ebean_createCopy(); + + if (nodeUsageCollector != null) { + nodeUsageCollector.setModified(); + } + } + + /** + * This is ONLY used for subclass entity beans. + *

    + * This is not used when entity bean classes are enhanced via javaagent or ant + * etc - only when a subclass is generated. + *

    + * Returns a Serializable instance that is either the 'byte code generated' + * object or a 'Vanilla' copy of this bean depending on + * SerializeControl.isVanillaBeans(). + */ + public Object writeReplaceIntercept() throws ObjectStreamException { + + if (!SerializeControl.isVanillaBeans()) { + return owner; + } + + // creates a plain vanilla object and + // copies the values from the owner + return owner._ebean_createCopy(); + } + + /** + * Helper method to check if two objects are equal. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected boolean areEqual(Object obj1, Object obj2) { + if (obj1 == null) { + return (obj2 == null); + } + if (obj2 == null) { + return false; + } + if (obj1 == obj2) { + return true; + } + if (obj1 instanceof BigDecimal) { + // Use comparable for BigDecimal as equals + // uses scale in comparison... + if (obj2 instanceof BigDecimal) { + Comparable com1 = (Comparable) obj1; + return (com1.compareTo(obj2) == 0); + + } else { + return false; + } + + } + if (obj1 instanceof URL) { + // use the string format to determine if dirty + return obj1.toString().equals(obj2.toString()); + } + return obj1.equals(obj2); + } + + /** + * Method that is called prior to a getter method on the actual entity. + *

    + * This checks if the bean is a reference and should be loaded. + *

    + */ + public void preGetter(String propertyName) { + if (!intercepting) { + return; + } + + if (!loaded) { + loadBean(propertyName); + } else if (loadedProps != null && !loadedProps.contains(propertyName)) { + loadBean(propertyName); + } + + if (nodeUsageCollector != null && loaded) { + nodeUsageCollector.addUsed(propertyName); + } + } + + /** + * Called for "enhancement" postSetter processing. This is around a PUTFIELD + * so no need to check the newValue afterwards. + */ + public void postSetter(PropertyChangeEvent event) { + if (pcs != null && event != null) { + pcs.firePropertyChange(event); + } + } + + /** + * Called for "subclassed" postSetter processing. Here the newValue has to be + * re-fetched (and passed into this method) in case there is code inside the + * setter that further mutates the value. + */ + public void postSetter(PropertyChangeEvent event, Object newValue) { + if (pcs != null && event != null) { + if (newValue != null && newValue.equals(event.getNewValue())) { + pcs.firePropertyChange(event); + } else { + pcs.firePropertyChange(event.getPropertyName(), event.getOldValue(), newValue); + } + } + } + + /** + * OneToMany and ManyToMany don't have any interception so just check for + * PropertyChangeSupport. + */ + public PropertyChangeEvent preSetterMany(boolean interceptField, String propertyName, + Object oldValue, Object newValue) { + + // skip setter interception on many's + if (pcs != null) { + return new PropertyChangeEvent(owner, propertyName, oldValue, newValue); + } else { + return null; + } + } + + private final void addDirty(String propertyName) { + + if (!intercepting) { + return; + } + if (readOnly) { + throw new IllegalStateException("This bean is readOnly"); + } + + if (loaded) { + if (oldValues == null) { + // first time this bean is being made dirty + createOldValues(); + } + if (changedProps == null) { + changedProps = new HashSet(); + } + changedProps.add(propertyName); + } + } + + /** + * Check to see if the values are not equal. If they are not equal then create + * the old values for use with ConcurrencyMode.ALL. + */ + public PropertyChangeEvent preSetter(boolean intercept, String propertyName, Object oldValue, + Object newValue) { + + boolean changed = !areEqual(oldValue, newValue); + + if (intercept && changed) { + addDirty(propertyName); + } + + if (changed && pcs != null) { + return new PropertyChangeEvent(owner, propertyName, oldValue, newValue); + } + + return null; + } + + /** + * Check for primitive boolean. + */ + public PropertyChangeEvent preSetter(boolean intercept, String propertyName, boolean oldValue, + boolean newValue) { + + boolean changed = oldValue != newValue; + + if (intercept && changed) { + addDirty(propertyName); + } + + if (changed && pcs != null) { + return new PropertyChangeEvent(owner, propertyName, Boolean.valueOf(oldValue), + Boolean.valueOf(newValue)); + } + + return null; + } + + /** + * Check for primitive int. + */ + public PropertyChangeEvent preSetter(boolean intercept, String propertyName, int oldValue, + int newValue) { + + boolean changed = oldValue != newValue; + + if (intercept && changed) { + addDirty(propertyName); + } + + if (changed && pcs != null) { + return new PropertyChangeEvent(owner, propertyName, Integer.valueOf(oldValue), + Integer.valueOf(newValue)); + } + return null; + } + + /** + * long. + */ + public PropertyChangeEvent preSetter(boolean intercept, String propertyName, long oldValue, + long newValue) { + + boolean changed = oldValue != newValue; + + if (intercept && changed) { + addDirty(propertyName); + } + + if (changed && pcs != null) { + return new PropertyChangeEvent(owner, propertyName, Long.valueOf(oldValue), + Long.valueOf(newValue)); + } + return null; + } + + /** + * double. + */ + public PropertyChangeEvent preSetter(boolean intercept, String propertyName, double oldValue, + double newValue) { + + boolean changed = oldValue != newValue; + + if (intercept && changed) { + addDirty(propertyName); + } + + if (changed && pcs != null) { + return new PropertyChangeEvent(owner, propertyName, Double.valueOf(oldValue), + Double.valueOf(newValue)); + } + return null; + } + + /** + * float. + */ + public PropertyChangeEvent preSetter(boolean intercept, String propertyName, float oldValue, + float newValue) { + + boolean changed = oldValue != newValue; + + if (intercept && changed) { + addDirty(propertyName); + } + + if (changed && pcs != null) { + return new PropertyChangeEvent(owner, propertyName, Float.valueOf(oldValue), + Float.valueOf(newValue)); + } + return null; + } + + /** + * short. + */ + public PropertyChangeEvent preSetter(boolean intercept, String propertyName, short oldValue, + short newValue) { + + boolean changed = oldValue != newValue; + + if (intercept && changed) { + addDirty(propertyName); + } + + if (changed && pcs != null) { + return new PropertyChangeEvent(owner, propertyName, Short.valueOf(oldValue), + Short.valueOf(newValue)); + } + return null; + } + + /** + * char. + */ + public PropertyChangeEvent preSetter(boolean intercept, String propertyName, char oldValue, + char newValue) { + + boolean changed = oldValue != newValue; + + if (intercept && changed) { + addDirty(propertyName); + } + + if (changed && pcs != null) { + return new PropertyChangeEvent(owner, propertyName, Character.valueOf(oldValue), + Character.valueOf(newValue)); + } + return null; + } + + /** + * char. + */ + public PropertyChangeEvent preSetter(boolean intercept, String propertyName, byte oldValue, + byte newValue) { + + boolean changed = oldValue != newValue; + + if (intercept && changed) { + addDirty(propertyName); + } + + if (changed && pcs != null) { + return new PropertyChangeEvent(owner, propertyName, Byte.valueOf(oldValue), + Byte.valueOf(newValue)); + } + return null; + } + + /** + * char[]. + */ + public PropertyChangeEvent preSetter(boolean intercept, String propertyName, char[] oldValue, + char[] newValue) { + + boolean changed = !areEqualChars(oldValue, newValue); + + if (intercept && changed) { + addDirty(propertyName); + } + + if (changed && pcs != null) { + return new PropertyChangeEvent(owner, propertyName, oldValue, newValue); + } + return null; + } + + /** + * byte[]. + */ + public PropertyChangeEvent preSetter(boolean intercept, String propertyName, byte[] oldValue, + byte[] newValue) { + + boolean changed = !areEqualBytes(oldValue, newValue); + + if (intercept && changed) { + addDirty(propertyName); + } + + if (changed && pcs != null) { + return new PropertyChangeEvent(owner, propertyName, oldValue, newValue); + } + return null; + } + + private static boolean areEqualBytes(byte[] b1, byte[] b2) { + if (b1 == null) { + return (b2 == null); + + } else if (b2 == null) { + return false; + + } else if (b1 == b2) { + return true; + + } else if (b1.length != b2.length) { + return false; + } + for (int i = 0; i < b1.length; i++) { + if (b1[i] != b2[i]) { + return false; + } + } + return true; + } + + private static boolean areEqualChars(char[] b1, char[] b2) { + if (b1 == null) { + return (b2 == null); + + } else if (b2 == null) { + return false; + + } else if (b1 == b2) { + return true; + + } else if (b1.length != b2.length) { + return false; + } + for (int i = 0; i < b1.length; i++) { + if (b1[i] != b2[i]) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/com/avaje/ebean/bean/NodeUsageCollector.java b/src/main/java/com/avaje/ebean/bean/NodeUsageCollector.java new file mode 100644 index 000000000..b50a5b1a2 --- /dev/null +++ b/src/main/java/com/avaje/ebean/bean/NodeUsageCollector.java @@ -0,0 +1,126 @@ +package com.avaje.ebean.bean; + +import java.lang.ref.WeakReference; +import java.util.HashSet; + +/** + * Collects profile information for a bean (or reference/proxy bean) at a given + * AutoFetchNode. + *

    + * The AutoFetchNode identifies the location of the bean in the object graph. + *

    + *

    + * It has to use a weak reference so as to ensure that it does not stop the + * associated bean from being garbage collected. + *

    + */ +public final class NodeUsageCollector { + + /** + * The point in the object graph for a specific query and call stack point. + */ + private final ObjectGraphNode node; + + /** + * Weak to allow garbage collection. + */ + private final WeakReference managerRef; + + /** + * The properties used at this profile point. + */ + private final HashSet used = new HashSet(); + + /** + * set to true if the bean is modified (setter called) + */ + private boolean modified; + + /** + * The property that cause a reference to lazy load. + */ + private String loadProperty; + + public NodeUsageCollector(ObjectGraphNode node, WeakReference managerRef) { + this.node = node; + // weak to allow garbage collection. + this.managerRef = managerRef; + } + + /** + * The bean has been modified by a setter method. + */ + public void setModified() { + modified = true; + } + + /** + * Add the name of a property that has been used. + */ + public void addUsed(String property) { + used.add(property); + } + + /** + * The property that invoked a lazy load. + */ + public void setLoadProperty(String loadProperty) { + this.loadProperty = loadProperty; + } + + /** + * Publish the usage info to the manager. + */ + private void publishUsageInfo() { + NodeUsageListener manager = managerRef.get(); + if (manager != null) { + manager.collectNodeUsage(this); + } + } + + /** + * publish the collected usage information when garbage collection occurs. + */ + @Override + protected void finalize() throws Throwable { + publishUsageInfo(); + super.finalize(); + } + + /** + * Return the associated node which identifies the location in the object + * graph of the bean/reference. + */ + public ObjectGraphNode getNode() { + return node; + } + + /** + * Return true if no properties where used. + */ + public boolean isEmpty() { + return used.isEmpty(); + } + + /** + * Return the set of used properties. + */ + public HashSet getUsed() { + return used; + } + + /** + * Return true if the bean was modified by a setter. + */ + public boolean isModified() { + return modified; + } + + public String getLoadProperty() { + return loadProperty; + } + + public String toString() { + return node + " read:" + used + " modified:" + modified; + } +} diff --git a/src/main/java/com/avaje/ebean/bean/NodeUsageListener.java b/src/main/java/com/avaje/ebean/bean/NodeUsageListener.java new file mode 100644 index 000000000..ee705a073 --- /dev/null +++ b/src/main/java/com/avaje/ebean/bean/NodeUsageListener.java @@ -0,0 +1,16 @@ +package com.avaje.ebean.bean; + +/** + * Collects the profile information. + */ +public interface NodeUsageListener { + + /** + * Collect node usage "profiling" information. + *

    + * This is the properties that are used for a given bean in the object graph. + * This information is used by autoFetch to tune queries. + *

    + */ + public void collectNodeUsage(NodeUsageCollector collector); +} diff --git a/src/main/java/com/avaje/ebean/bean/ObjectGraphNode.java b/src/main/java/com/avaje/ebean/bean/ObjectGraphNode.java new file mode 100644 index 000000000..031f91e22 --- /dev/null +++ b/src/main/java/com/avaje/ebean/bean/ObjectGraphNode.java @@ -0,0 +1,69 @@ +package com.avaje.ebean.bean; + +import java.io.Serializable; + +/** + * Identifies a unique node of an object graph. + *

    + * It represents a location relative to the root of an object graph and specific + * to a query and call stack hash. + *

    + */ +public final class ObjectGraphNode implements Serializable { + + private static final long serialVersionUID = 2087081778650228996L; + + /** + * Identifies the origin. + */ + private final ObjectGraphOrigin originQueryPoint; + + /** + * The path relative to the root. + */ + private final String path; + + /** + * Create at a sub level. + */ + public ObjectGraphNode(ObjectGraphNode parent, String path) { + this.originQueryPoint = parent.getOriginQueryPoint(); + this.path = parent.getChildPath(path); + } + + /** + * Create an the root level. + */ + public ObjectGraphNode(ObjectGraphOrigin originQueryPoint, String path) { + this.originQueryPoint = originQueryPoint; + this.path = path; + } + + /** + * Return the origin query point. + */ + public ObjectGraphOrigin getOriginQueryPoint() { + return originQueryPoint; + } + + private String getChildPath(String childPath) { + if (path == null) { + return childPath; + } else if (childPath == null) { + return path; + } else { + return path + "." + childPath; + } + } + + /** + * Return the path relative to the root. + */ + public String getPath() { + return path; + } + + public String toString() { + return "origin:" + originQueryPoint + " " + ":" + path + ":" + path; + } +} diff --git a/src/main/java/com/avaje/ebean/bean/ObjectGraphOrigin.java b/src/main/java/com/avaje/ebean/bean/ObjectGraphOrigin.java new file mode 100644 index 000000000..9c9f97635 --- /dev/null +++ b/src/main/java/com/avaje/ebean/bean/ObjectGraphOrigin.java @@ -0,0 +1,61 @@ +package com.avaje.ebean.bean; + +import java.io.Serializable; + +/** + * Represents a "origin" of an ORM object graph. This combines the call stack + * and query plan hash. + *

    + * The call stack is included so that the query can have different tuned fetches + * for each unique call stack. For example, a query to fetch a customer could be + * called by three different methods and each can be treated as a separate + * origin point (and autoFetch can tune each one separately). + *

    + */ +public final class ObjectGraphOrigin implements Serializable { + + private static final long serialVersionUID = 410937765287968707L; + + private final CallStack callStack; + + private final String key; + + private final String beanType; + + public ObjectGraphOrigin(int queryHash, CallStack callStack, String beanType) { + this.callStack = callStack; + this.beanType = beanType; + this.key = callStack.getOriginKey(queryHash); + } + + /** + * The key includes the queryPlan hash and the callStack hash. This becomes + * the unique identifier for a query point. + */ + public String getKey() { + return key; + } + + /** + * The type of bean the query is fetching. + */ + public String getBeanType() { + return beanType; + } + + /** + * The call stack involved. + */ + public CallStack getCallStack() { + return callStack; + } + + public String getFirstStackElement() { + return callStack.getFirstStackTraceElement().toString(); + } + + public String toString() { + return key + " " + beanType + " " + callStack.getFirstStackTraceElement(); + } + +} diff --git a/src/main/java/com/avaje/ebean/bean/PersistenceContext.java b/src/main/java/com/avaje/ebean/bean/PersistenceContext.java new file mode 100644 index 000000000..01bb2eb25 --- /dev/null +++ b/src/main/java/com/avaje/ebean/bean/PersistenceContext.java @@ -0,0 +1,52 @@ +package com.avaje.ebean.bean; + +/** + * Holds entity beans by there type and id. + *

    + * This is used to ensure only one instance for a given entity type and id is + * used to build object graphs from queries and lazy loading. + *

    + */ +public interface PersistenceContext { + + /** + * Put the entity bean into the PersistanceContext. + */ + public void put(Object id, Object bean); + + /** + * Put the entity bean into the PersistanceContext if one is not already + * present (for this id). + *

    + * Returns an existing entity bean (if one is already there) and otherwise + * returns null. + *

    + */ + public Object putIfAbsent(Object id, Object bean); + + /** + * Return an object given its type and unique id. + */ + public Object get(Class beanType, Object uid); + + /** + * Clear all the references. + */ + public void clear(); + + /** + * Clear all the references for a given type of entity bean. + */ + public void clear(Class beanType); + + /** + * Clear the reference to a specific entity bean. + */ + public void clear(Class beanType, Object uid); + + /** + * Return the number of beans of the given type in the persistence context. + */ + public int size(Class beanType); + +} diff --git a/src/main/java/com/avaje/ebean/bean/SerializeControl.java b/src/main/java/com/avaje/ebean/bean/SerializeControl.java new file mode 100644 index 000000000..e1c829a15 --- /dev/null +++ b/src/main/java/com/avaje/ebean/bean/SerializeControl.java @@ -0,0 +1,130 @@ +package com.avaje.ebean.bean; + +/** + * This is ONLY used for subclassed entity beans. + *

    + * This is NOT USED for entity beans that are enhanced via a javaagent or ant + * task etc. This is only used when the entity beans are created as a subclass + * of the original class. + *

    + *

    + * Allows the developer to control whether beans and collections are serialized + * to plain 'vanilla' classes or left in byte code generated subclasses. + *

    + * Vanilla beans are beans that have plain ordinary classes as opposed to + * specially generated classes that Ebean creates. Ebean creates classes (using + * ASM) to support lazy loading (reference beans) and concurrency checking etc. + *

    + *

    + * SerializeControl gives you the ability to control whether an object graph is + * serialized to plain 'vanilla' objects or in the special byte code generated + * form. There are pros and cons for both approaches depending on whether you + * want to support "FULL" concurrency checking and lazy loading when the object + * graph is deserialized. + *

    + *

    + * Note that BeanMap, BeanList and BeanSet are not byte code generated. They are + * ordinary classes. However you may wish to have these serialized to the + * underlying List Set and Map implementations for the benefit that they can be + * deserialised in a JVM without ANY ebean code at all. + *

    + */ +public class SerializeControl { + + private static final String BEANS = "com.avaje.ebean.vanillabeans"; + + private static final String COLLECTIONS = "com.avaje.ebean.vanillacollections"; + + private static Boolean getDefault(String key, Boolean dflt) { + String val = System.getProperty(key); + if (val != null) { + return val.equalsIgnoreCase("true"); + } + return dflt; + } + + private static ThreadLocal vanillaBeans = new ThreadLocal() { + protected synchronized Boolean initialValue() { + return getDefault(BEANS, Boolean.TRUE); + } + }; + + private static ThreadLocal vanillaCollections = new ThreadLocal() { + protected synchronized Boolean initialValue() { + return getDefault(COLLECTIONS, Boolean.TRUE); + } + }; + + /** + * Set the JVM wide default for Beans. + */ + public static void setDefaultForBeans(boolean vanillaOn) { + Boolean b = Boolean.valueOf(vanillaOn); + System.setProperty(BEANS, b.toString()); + } + + /** + * Set the JVM wide default for Collections. + */ + public static void setDefaultForCollections(boolean vanillaOn) { + Boolean b = Boolean.valueOf(vanillaOn); + System.setProperty(COLLECTIONS, b.toString()); + } + + /** + * Reset the mode for beans and collections back to the JVM wide default + * setting. + */ + public static void resetToDefault() { + Boolean beans = getDefault(BEANS, Boolean.FALSE); + setVanillaBeans(beans); + + Boolean coll = getDefault(COLLECTIONS, Boolean.FALSE); + setVanillaCollections(coll); + } + + /** + * Set the mode for both Beans and Collections. + */ + public static void setVanilla(boolean vanillaOn) { + if (vanillaOn) { + vanillaBeans.set(Boolean.TRUE); + vanillaCollections.set(Boolean.TRUE); + } else { + vanillaBeans.set(Boolean.FALSE); + vanillaCollections.set(Boolean.FALSE); + } + } + + /** + * Return true if beans are serialized to Vanilla as opposed to byte code + * generated subclasses. + */ + public static boolean isVanillaBeans() { + return (Boolean) vanillaBeans.get(); + } + + /** + * Set whether beans should be serialized to Vanilla as opposed to byte code + * generated subclasses. + */ + public static void setVanillaBeans(boolean vanillaOn) { + vanillaBeans.set(vanillaOn); + } + + /** + * Return true if collections are serialized to be plain Lists Sets or Maps as + * opposed to BeanList, BeanMap or BeanSet. + */ + public static boolean isVanillaCollections() { + return (Boolean) vanillaCollections.get(); + } + + /** + * Set whether collections should be serialized to Vanilla Lists Sets or Maps + * (instead of BeanList, BeanMap or BeanSet). + */ + public static void setVanillaCollections(boolean vanillaOn) { + vanillaCollections.set(vanillaOn); + } +} diff --git a/src/main/java/com/avaje/ebean/bean/package.html b/src/main/java/com/avaje/ebean/bean/package.html new file mode 100644 index 000000000..6abde55f5 --- /dev/null +++ b/src/main/java/com/avaje/ebean/bean/package.html @@ -0,0 +1,12 @@ + + + + Enhanced beans API and Support objects + + +Enhanced beans API and Support objects + + + + + \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/cache/ServerCache.java b/src/main/java/com/avaje/ebean/cache/ServerCache.java new file mode 100644 index 000000000..099e9122e --- /dev/null +++ b/src/main/java/com/avaje/ebean/cache/ServerCache.java @@ -0,0 +1,89 @@ +package com.avaje.ebean.cache; + +import com.avaje.ebean.EbeanServer; + +/** + * Represents part of the "L2" server side cache. + *

    + * This is used to cache beans or query results (bean collections). + *

    + *

    + * There are 2 ServerCache's for each bean type. One is used as the 'bean cache' + * which holds beans of a given type. The other is the 'query cache' holding + * query results for a given type. + *

    + * + * @author rbygrave + */ +public interface ServerCache { + + /** + * Just after a cache is created this init method is called. This is so that a + * cache implementation can make use of the BackgroundExecutor service to + * trim/cleanup itself or use the EbeanServer to populate itself. + *

    + * This method is called after the cache is constructed but before the cache + * is made available for use. + *

    + */ + public void init(EbeanServer ebeanServer); + + /** + * Return the configuration options for this cache. + */ + public ServerCacheOptions getOptions(); + + /** + * Update the configuration options for this cache. + */ + public void setOptions(ServerCacheOptions options); + + /** + * Return the value given the key. + */ + public Object get(Object id); + + /** + * Put the value in the cache with a given id. + */ + public Object put(Object id, Object value); + + /** + * Put the value in the cache but only if a matching value is not already in + * the cache. + */ + public Object putIfAbsent(Object id, Object value); + + /** + * Remove a entry from the cache given its id. + */ + public Object remove(Object id); + + /** + * Clear all entries from the cache. + *

    + * NOTE: Be careful using this method in that most of the time application + * code should clear BOTH the bean and query caches at the same time. This can + * be done via {@link ServerCacheManager#clear(Class)}. + *

    + */ + public void clear(); + + /** + * Return the number of entries in the cache. + */ + public int size(); + + /** + * Return the hit ratio the cache is currently getting. + */ + public int getHitRatio(); + + /** + * Return statistics for the cache. + * + * @param reset + * if true the statistics are reset. + */ + public ServerCacheStatistics getStatistics(boolean reset); +} diff --git a/src/main/java/com/avaje/ebean/cache/ServerCacheFactory.java b/src/main/java/com/avaje/ebean/cache/ServerCacheFactory.java new file mode 100644 index 000000000..2b36081ac --- /dev/null +++ b/src/main/java/com/avaje/ebean/cache/ServerCacheFactory.java @@ -0,0 +1,26 @@ +package com.avaje.ebean.cache; + +import com.avaje.ebean.EbeanServer; + +/** + * Defines method for constructing caches for beans and queries. + */ +public interface ServerCacheFactory { + + /** + * Just after the ServerCacheFactory is constructed this method is called + * passing the EbeanServer. + *

    + * This is so that a cache implementation can utilise the EbeanServer to + * populate itself or use the BackgroundExecutor service to schedule periodic + * cache trimming/cleanup. + *

    + */ + public void init(EbeanServer ebeanServer); + + /** + * Create the cache for the given type with options. + */ + public ServerCache createCache(String cacheKey, ServerCacheOptions cacheOptions); + +} diff --git a/src/main/java/com/avaje/ebean/cache/ServerCacheManager.java b/src/main/java/com/avaje/ebean/cache/ServerCacheManager.java new file mode 100644 index 000000000..4767db886 --- /dev/null +++ b/src/main/java/com/avaje/ebean/cache/ServerCacheManager.java @@ -0,0 +1,55 @@ +package com.avaje.ebean.cache; + +import com.avaje.ebean.EbeanServer; + +/** + * The cache service for server side caching of beans and query results. + */ +public interface ServerCacheManager { + + /** + * This method is called just after the construction of the + * ServerCacheManager. + *

    + * The EbeanServer is provided so that cache implementations can make use of + * EbeanServer and BackgroundExecutor for automatically populating and + * background trimming of the cache. + *

    + */ + public void init(EbeanServer server); + + public void setCaching(Class beanType, boolean useCache); + + /** + * Return true if there is an active bean cache for this type of bean. + */ + public boolean isBeanCaching(Class beanType); + + /** + * Return the cache for mapping natural keys to id values. + */ + public ServerCache getNaturalKeyCache(Class beanType); + + /** + * Return the cache for beans of a particular type. + */ + public ServerCache getBeanCache(Class beanType); + + public ServerCache getCollectionIdsCache(Class beanType, String propertyName); + + /** + * Return the cache for query results of a particular type of bean. + */ + public ServerCache getQueryCache(Class beanType); + + /** + * This clears both the bean and query cache for a given type. + */ + public void clear(Class beanType); + + /** + * Clear all the caches. + */ + public void clearAll(); + +} diff --git a/src/main/java/com/avaje/ebean/cache/ServerCacheOptions.java b/src/main/java/com/avaje/ebean/cache/ServerCacheOptions.java new file mode 100644 index 000000000..181c85869 --- /dev/null +++ b/src/main/java/com/avaje/ebean/cache/ServerCacheOptions.java @@ -0,0 +1,110 @@ +package com.avaje.ebean.cache; + +import com.avaje.ebean.annotation.CacheTuning; + +/** + * Options for controlling a cache. + */ +public class ServerCacheOptions { + + private int maxSize; + private int maxIdleSecs; + private int maxSecsToLive; + + /** + * Construct with no set options. + */ + public ServerCacheOptions() { + + } + + /** + * Create from the cacheTuning deployment annotation. + */ + public ServerCacheOptions(CacheTuning cacheTuning) { + this.maxSize = cacheTuning.maxSize(); + this.maxIdleSecs = cacheTuning.maxIdleSecs(); + this.maxSecsToLive = cacheTuning.maxSecsToLive(); + } + + /** + * Create merging default options with the deployment specified ones. + */ + public ServerCacheOptions(ServerCacheOptions d) { + this.maxSize = d.getMaxSize(); + this.maxIdleSecs = d.getMaxIdleSecs(); + this.maxSecsToLive = d.getMaxIdleSecs(); + } + + /** + * Apply any settings from the default settings that have not already been + * specifically set. + */ + public void applyDefaults(ServerCacheOptions defaults) { + if (maxSize == 0) { + maxSize = defaults.getMaxSize(); + } + if (maxIdleSecs == 0) { + maxIdleSecs = defaults.getMaxIdleSecs(); + } + if (maxSecsToLive == 0) { + maxSecsToLive = defaults.getMaxSecsToLive(); + } + } + + /** + * Return a copy of this object. + */ + public ServerCacheOptions copy() { + + ServerCacheOptions copy = new ServerCacheOptions(); + copy.maxSize = maxSize; + copy.maxIdleSecs = maxIdleSecs; + copy.maxSecsToLive = maxSecsToLive; + + return copy; + } + + /** + * Return the maximum cache size. + */ + public int getMaxSize() { + return maxSize; + } + + /** + * Set the maximum cache size. + */ + public void setMaxSize(int maxSize) { + this.maxSize = maxSize; + } + + /** + * Return the maximum idle time. + */ + public int getMaxIdleSecs() { + return maxIdleSecs; + } + + /** + * Set the maximum idle time. + */ + public void setMaxIdleSecs(int maxIdleSecs) { + this.maxIdleSecs = maxIdleSecs; + } + + /** + * Return the maximum time to live. + */ + public int getMaxSecsToLive() { + return maxSecsToLive; + } + + /** + * Set the maximum time to live. + */ + public void setMaxSecsToLive(int maxSecsToLive) { + this.maxSecsToLive = maxSecsToLive; + } + +} diff --git a/src/main/java/com/avaje/ebean/cache/ServerCacheStatistics.java b/src/main/java/com/avaje/ebean/cache/ServerCacheStatistics.java new file mode 100644 index 000000000..60f8eb587 --- /dev/null +++ b/src/main/java/com/avaje/ebean/cache/ServerCacheStatistics.java @@ -0,0 +1,124 @@ +package com.avaje.ebean.cache; + +/** + * The statistics collected per cache. + *

    + * These can be monitored to review the effectiveness of a particular cache. + *

    + * + * @author rbygrave + * + */ +public class ServerCacheStatistics { + + protected String cacheName; + + protected int maxSize; + + protected int size; + + protected int hitCount; + + protected int missCount; + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(cacheName); + sb.append(" size:").append(size); + sb.append(" hitRatio:").append(getHitRatio()); + sb.append(" hitCount:").append(hitCount); + sb.append(" missCount:").append(missCount); + sb.append(" maxSize:").append(maxSize); + return sb.toString(); + } + + /** + * Return the name of the cache. + */ + public String getCacheName() { + return cacheName; + } + + /** + * Set the name of the cache. + */ + public void setCacheName(String cacheName) { + this.cacheName = cacheName; + } + + /** + * Return the hit count. The number of successful gets. + */ + public int getHitCount() { + return hitCount; + } + + /** + * Set the hit count. + */ + public void setHitCount(int hitCount) { + this.hitCount = hitCount; + } + + /** + * Return the miss count. The number of gets that returned null. + */ + public int getMissCount() { + return missCount; + } + + /** + * Set the miss count. + */ + public void setMissCount(int missCount) { + this.missCount = missCount; + } + + /** + * Return the size of the cache. + */ + public int getSize() { + return size; + } + + /** + * Set the size of the cache. + */ + public void setSize(int size) { + this.size = size; + } + + /** + * Return the maximum size of the cache. + *

    + * Can be used in conjunction with the size to determine if the cache use is + * being potentially limited by its maximum size. + *

    + */ + public int getMaxSize() { + return maxSize; + } + + /** + * Set the maximum size of the cache. + */ + public void setMaxSize(int maxSize) { + this.maxSize = maxSize; + } + + /** + * Returns an int from 0 to 100 (percentage) for the hit ratio. + *

    + * A hit ratio of 100 means every get request against the cache hits an entry. + *

    + */ + public int getHitRatio() { + int totalCount = hitCount + missCount; + if (totalCount == 0) { + return 0; + } else { + return hitCount * 100 / totalCount; + } + } + +} diff --git a/src/main/java/com/avaje/ebean/cache/package.html b/src/main/java/com/avaje/ebean/cache/package.html new file mode 100644 index 000000000..e69e39f09 --- /dev/null +++ b/src/main/java/com/avaje/ebean/cache/package.html @@ -0,0 +1,10 @@ + + + + Server Cache Service + + +Server Cache Service + + + \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/common/AbstractBeanCollection.java b/src/main/java/com/avaje/ebean/common/AbstractBeanCollection.java new file mode 100644 index 000000000..a9cacd874 --- /dev/null +++ b/src/main/java/com/avaje/ebean/common/AbstractBeanCollection.java @@ -0,0 +1,290 @@ +package com.avaje.ebean.common; + +import java.util.Set; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import javax.persistence.PersistenceException; + +import com.avaje.ebean.Ebean; +import com.avaje.ebean.ExpressionList; +import com.avaje.ebean.bean.BeanCollection; +import com.avaje.ebean.bean.BeanCollectionLoader; +import com.avaje.ebean.bean.BeanCollectionTouched; +import com.avaje.ebean.bean.EntityBean; +import com.avaje.ebean.bean.EntityBeanIntercept; + +/** + * Base class for List Set and Map implementations of BeanCollection. + * + * @author rbygrave + */ +public abstract class AbstractBeanCollection implements BeanCollection { + + private static final long serialVersionUID = 3365725236140187588L; + + protected boolean readOnly; + + /** + * The EbeanServer this is associated with. (used for lazy fetch). + */ + protected transient BeanCollectionLoader loader; + + protected transient ExpressionList filterMany; + + protected int loaderIndex; + + protected String ebeanServerName; + + protected transient BeanCollectionTouched beanCollectionTouched; + + protected transient Future fetchFuture; + + /** + * The owning bean (used for lazy fetch). + */ + protected final Object ownerBean; + + /** + * The name of this property in the owning bean (used for lazy fetch). + */ + protected final String propertyName; + + /** + * Can be false when a background thread is used to continue the fetch the + * rows. It will set this to true when it is finished. If no background thread + * is used then this should already be true. + */ + protected boolean finishedFetch = true; + + /** + * Flag set to true if rows are limited by firstRow maxRows and more rows + * exist. For use by client to enable 'next' for paging. + */ + protected boolean hasMoreRows; + + protected ModifyHolder modifyHolder; + + protected ModifyListenMode modifyListenMode; + protected boolean modifyAddListening; + protected boolean modifyRemoveListening; + protected boolean modifyListening; + + /** + * Constructor not non-lazy loading collection. + */ + public AbstractBeanCollection() { + this.ownerBean = null; + this.propertyName = null; + } + + /** + * Used to create deferred fetch proxy. + */ + public AbstractBeanCollection(BeanCollectionLoader loader, Object ownerBean, String propertyName) { + this.loader = loader; + this.ebeanServerName = loader.getName(); + this.ownerBean = ownerBean; + this.propertyName = propertyName; + + if (ownerBean instanceof EntityBean) { + EntityBeanIntercept ebi = ((EntityBean) ownerBean)._ebean_getIntercept(); + this.readOnly = ebi.isReadOnly(); + } + } + + public Object getOwnerBean() { + return ownerBean; + } + + public String getPropertyName() { + return propertyName; + } + + public int getLoaderIndex() { + return loaderIndex; + } + + public ExpressionList getFilterMany() { + return filterMany; + } + + public void setFilterMany(ExpressionList filterMany) { + this.filterMany = filterMany; + } + + protected void lazyLoadCollection(boolean onlyIds) { + if (loader == null) { + loader = (BeanCollectionLoader) Ebean.getServer(ebeanServerName); + } + if (loader == null) { + String msg = "Lazy loading but LazyLoadEbeanServer is null?" + + " The LazyLoadEbeanServer needs to be set after deserialization" + + " to support lazy loading."; + throw new PersistenceException(msg); + } + + loader.loadMany(this, onlyIds); + checkEmptyLazyLoad(); + } + + protected void touched() { + if (beanCollectionTouched != null) { + // only call this once + beanCollectionTouched.notifyTouched(this); + beanCollectionTouched = null; + } + } + + public void setBeanCollectionTouched(BeanCollectionTouched notify) { + this.beanCollectionTouched = notify; + } + + public void setLoader(int beanLoaderIndex, BeanCollectionLoader loader) { + this.loaderIndex = beanLoaderIndex; + this.loader = loader; + this.ebeanServerName = loader.getName(); + } + + public boolean isReadOnly() { + return readOnly; + } + + public void setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + } + + /** + * Set to true if maxRows was hit and there are actually more rows available. + *

    + * Can be used by client code that is paging through results using + * setFirstRow() setMaxRows(). If this returns true then the client can + * display a 'next' button etc. + *

    + */ + public boolean hasMoreRows() { + return hasMoreRows; + } + + /** + * Set to true when maxRows is hit but there are actually more rows available. + * This is set so that client code knows that there is more data available. + */ + public void setHasMoreRows(boolean hasMoreRows) { + this.hasMoreRows = hasMoreRows; + } + + /** + * Returns true if the fetch has finished. False if the fetch is continuing in + * a background thread. + */ + public boolean isFinishedFetch() { + return finishedFetch; + } + + /** + * Set to true when a fetch has finished. Used when a fetch continues in the + * background. + */ + public void setFinishedFetch(boolean finishedFetch) { + this.finishedFetch = finishedFetch; + } + + public void setBackgroundFetch(Future fetchFuture) { + this.fetchFuture = fetchFuture; + } + + public void backgroundFetchWait(long wait, TimeUnit timeUnit) { + if (fetchFuture != null) { + try { + fetchFuture.get(wait, timeUnit); + } catch (Exception e) { + throw new PersistenceException(e); + } + } + } + + public void backgroundFetchWait() { + if (fetchFuture != null) { + try { + fetchFuture.get(); + } catch (Exception e) { + throw new PersistenceException(e); + } + } + } + + protected void checkReadOnly() { + if (readOnly) { + String msg = "This collection is in ReadOnly mode"; + throw new IllegalStateException(msg); + } + } + + // --------------------------------------------------------- + // Support for modify additions deletions etc - ManyToMany + // --------------------------------------------------------- + + /** + * set modifyListening to be on or off. + */ + public void setModifyListening(ModifyListenMode mode) { + + this.modifyListenMode = mode; + this.modifyAddListening = ModifyListenMode.ALL.equals(mode); + this.modifyRemoveListening = modifyAddListening || ModifyListenMode.REMOVALS.equals(mode); + this.modifyListening = modifyRemoveListening || modifyAddListening; + if (modifyListening) { + // lose any existing modifications + modifyHolder = null; + } + } + + /** + * Return the modify listening mode this collection is using. + */ + public ModifyListenMode getModifyListenMode() { + return modifyListenMode; + } + + protected ModifyHolder getModifyHolder() { + if (modifyHolder == null) { + modifyHolder = new ModifyHolder(); + } + return modifyHolder; + } + + public void modifyAddition(E bean) { + if (modifyAddListening) { + getModifyHolder().modifyAddition(bean); + } + } + + public void modifyRemoval(Object bean) { + if (modifyRemoveListening) { + getModifyHolder().modifyRemoval(bean); + } + } + + public void modifyReset() { + if (modifyHolder != null) { + modifyHolder.reset(); + } + } + + public Set getModifyAdditions() { + if (modifyHolder == null) { + return null; + } else { + return modifyHolder.getModifyAdditions(); + } + } + + public Set getModifyRemovals() { + if (modifyHolder == null) { + return null; + } else { + return modifyHolder.getModifyRemovals(); + } + } +} diff --git a/src/main/java/com/avaje/ebean/common/BeanList.java b/src/main/java/com/avaje/ebean/common/BeanList.java new file mode 100644 index 000000000..7559b4b22 --- /dev/null +++ b/src/main/java/com/avaje/ebean/common/BeanList.java @@ -0,0 +1,459 @@ +package com.avaje.ebean.common; + +import java.io.ObjectStreamException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import com.avaje.ebean.bean.BeanCollectionAdd; +import com.avaje.ebean.bean.BeanCollectionLoader; +import com.avaje.ebean.bean.SerializeControl; + +/** + * List capable of lazy loading. + */ +public final class BeanList extends AbstractBeanCollection implements List, + BeanCollectionAdd { + + /** + * The underlying List implementation. + */ + private List list; + + /** + * Specify the underlying List implementation. + */ + public BeanList(List list) { + super(); + this.list = list; + } + + /** + * Uses an ArrayList as the underlying List implementation. + */ + public BeanList() { + this(new ArrayList()); + } + + /** + * Used to create deferred fetch proxy. + */ + public BeanList(BeanCollectionLoader loader, Object ownerBean, String propertyName) { + super(loader, ownerBean, propertyName); + } + + Object readResolve() throws ObjectStreamException { + if (SerializeControl.isVanillaCollections()) { + return list; + } + return this; + } + + Object writeReplace() throws ObjectStreamException { + if (SerializeControl.isVanillaCollections()) { + return list; + } + return this; + } + + @SuppressWarnings("unchecked") + public void addBean(Object bean) { + list.add((E) bean); + } + + @SuppressWarnings("unchecked") + public void internalAdd(Object bean) { + list.add((E) bean); + } + + public boolean checkEmptyLazyLoad() { + if (list == null) { + list = new ArrayList(); + return true; + } else { + return false; + } + } + + private void initClear() { + synchronized (this) { + if (list == null) { + if (modifyListening) { + lazyLoadCollection(true); + } else { + list = new ArrayList(); + } + } + touched(); + } + } + + private void init() { + synchronized (this) { + if (list == null) { + lazyLoadCollection(false); + } + touched(); + } + } + + /** + * Set the actual underlying list. + *

    + * This is primarily for the deferred fetching function. + *

    + */ + @SuppressWarnings("unchecked") + public void setActualList(List list) { + this.list = (List) list; + } + + /** + * Return the actual underlying list. + */ + public List getActualList() { + return list; + } + + public Collection getActualDetails() { + return list; + } + + /** + * Returns the underlying list. + */ + public Object getActualCollection() { + return list; + } + + /** + * Return true if the underlying list is populated. + */ + public boolean isPopulated() { + return list != null; + } + + /** + * Return true if this is a reference (lazy loading) bean collection. This is + * the same as !isPopulated(); + */ + public boolean isReference() { + return list == null; + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("BeanList "); + if (isReadOnly()) { + sb.append("readOnly "); + } + if (list == null) { + sb.append("deferred "); + + } else { + sb.append("size[").append(list.size()).append("] "); + sb.append("hasMoreRows[").append(hasMoreRows).append("] "); + sb.append("list").append(list).append(""); + } + return sb.toString(); + } + + /** + * Equal if obj is a List and equal in a list sense. + *

    + * Specifically obj does not need to be a BeanList but any list. This does not + * use the FindMany, fetchedMaxRows or finishedFetch properties in the equals + * test. + *

    + */ + public boolean equals(Object obj) { + init(); + return list.equals(obj); + } + + public int hashCode() { + init(); + return list.hashCode(); + } + + // -----------------------------------------------------// + // The additional methods are here + // -----------------------------------------------------// + + // -----------------------------------------------------// + // proxy method for List + // -----------------------------------------------------// + + public void add(int index, E element) { + checkReadOnly(); + init(); + if (modifyAddListening) { + modifyAddition(element); + } + list.add(index, element); + } + + public boolean add(E o) { + checkReadOnly(); + init(); + if (modifyAddListening) { + if (list.add(o)) { + modifyAddition(o); + return true; + } else { + return false; + } + } + return list.add(o); + } + + public boolean addAll(Collection c) { + checkReadOnly(); + init(); + if (modifyAddListening) { + // all elements in c are added (no contains checking) + getModifyHolder().modifyAdditionAll(c); + } + return list.addAll(c); + } + + public boolean addAll(int index, Collection c) { + checkReadOnly(); + init(); + if (modifyAddListening) { + // all elements in c are added (no contains checking) + getModifyHolder().modifyAdditionAll(c); + } + return list.addAll(index, c); + } + + public void clear() { + checkReadOnly(); + // TODO: when clear() and not initialised could be more clever + // and fetch just the Id's + initClear(); + if (modifyRemoveListening) { + for (int i = 0; i < list.size(); i++) { + getModifyHolder().modifyRemoval(list.get(i)); + } + } + list.clear(); + } + + public boolean contains(Object o) { + init(); + return list.contains(o); + } + + public boolean containsAll(Collection c) { + init(); + return list.containsAll(c); + } + + public E get(int index) { + init(); + return list.get(index); + } + + public int indexOf(Object o) { + init(); + return list.indexOf(o); + } + + public boolean isEmpty() { + init(); + return list.isEmpty(); + } + + public Iterator iterator() { + init(); + if (isReadOnly()) { + return new ReadOnlyListIterator(list.listIterator()); + } + if (modifyListening) { + Iterator it = list.iterator(); + return new ModifyIterator(this, it); + } + return list.iterator(); + } + + public int lastIndexOf(Object o) { + init(); + return list.lastIndexOf(o); + } + + public ListIterator listIterator() { + init(); + if (isReadOnly()) { + return new ReadOnlyListIterator(list.listIterator()); + } + if (modifyListening) { + ListIterator it = list.listIterator(); + return new ModifyListIterator(this, it); + } + return list.listIterator(); + } + + public ListIterator listIterator(int index) { + init(); + if (isReadOnly()) { + return new ReadOnlyListIterator(list.listIterator(index)); + } + if (modifyListening) { + ListIterator it = list.listIterator(index); + return new ModifyListIterator(this, it); + } + return list.listIterator(index); + } + + public E remove(int index) { + checkReadOnly(); + init(); + if (modifyRemoveListening) { + E o = list.remove(index); + modifyRemoval(o); + return o; + } + return list.remove(index); + } + + public boolean remove(Object o) { + checkReadOnly(); + init(); + if (modifyRemoveListening) { + boolean isRemove = list.remove(o); + if (isRemove) { + modifyRemoval(o); + } + return isRemove; + } + return list.remove(o); + } + + public boolean removeAll(Collection c) { + checkReadOnly(); + init(); + if (modifyRemoveListening) { + boolean changed = false; + Iterator it = c.iterator(); + while (it.hasNext()) { + Object o = (Object) it.next(); + if (list.remove(o)) { + modifyRemoval(o); + changed = true; + } + } + return changed; + } + return list.removeAll(c); + } + + public boolean retainAll(Collection c) { + checkReadOnly(); + init(); + if (modifyRemoveListening) { + boolean changed = false; + Iterator it = list.iterator(); + while (it.hasNext()) { + Object o = (Object) it.next(); + if (!c.contains(o)) { + it.remove(); + modifyRemoval(o); + changed = true; + } + } + return changed; + } + return list.retainAll(c); + } + + public E set(int index, E element) { + checkReadOnly(); + init(); + if (modifyListening) { + E o = list.set(index, element); + modifyAddition(element); + modifyRemoval(o); + return o; + } + return list.set(index, element); + } + + public int size() { + init(); + return list.size(); + } + + public List subList(int fromIndex, int toIndex) { + init(); + if (isReadOnly()) { + return Collections.unmodifiableList(list.subList(fromIndex, toIndex)); + } + if (modifyListening) { + return new ModifyList(this, list.subList(fromIndex, toIndex)); + } + return list.subList(fromIndex, toIndex); + } + + public Object[] toArray() { + init(); + return list.toArray(); + } + + public T[] toArray(T[] a) { + init(); + return list.toArray(a); + } + + private static class ReadOnlyListIterator implements ListIterator, Serializable { + + private static final long serialVersionUID = 3097271091406323699L; + + private final ListIterator i; + + ReadOnlyListIterator(ListIterator i) { + this.i = i; + } + + public void add(E o) { + throw new IllegalStateException("This collection is in ReadOnly mode"); + } + + public void remove() { + throw new IllegalStateException("This collection is in ReadOnly mode"); + } + + public void set(E o) { + throw new IllegalStateException("This collection is in ReadOnly mode"); + } + + public boolean hasNext() { + return i.hasNext(); + } + + public boolean hasPrevious() { + return i.hasPrevious(); + } + + public E next() { + return i.next(); + } + + public int nextIndex() { + return i.nextIndex(); + } + + public E previous() { + return i.previous(); + } + + public int previousIndex() { + return i.previousIndex(); + } + + } +} diff --git a/src/main/java/com/avaje/ebean/common/BeanMap.java b/src/main/java/com/avaje/ebean/common/BeanMap.java new file mode 100644 index 000000000..ea9601195 --- /dev/null +++ b/src/main/java/com/avaje/ebean/common/BeanMap.java @@ -0,0 +1,279 @@ +package com.avaje.ebean.common; + +import java.io.ObjectStreamException; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import com.avaje.ebean.bean.BeanCollectionLoader; +import com.avaje.ebean.bean.SerializeControl; + +/** + * Map capable of lazy loading. + */ +public final class BeanMap extends AbstractBeanCollection implements Map { + + /** + * The underlying map implementation. + */ + private Map map; + + /** + * Create with a given Map. + */ + public BeanMap(Map map) { + this.map = map; + } + + /** + * Create using a underlying LinkedHashMap. + */ + public BeanMap() { + this(new LinkedHashMap()); + } + + public BeanMap(BeanCollectionLoader ebeanServer, Object ownerBean, String propertyName) { + super(ebeanServer, ownerBean, propertyName); + } + + Object readResolve() throws ObjectStreamException { + if (SerializeControl.isVanillaCollections()) { + return map; + } + return this; + } + + Object writeReplace() throws ObjectStreamException { + if (SerializeControl.isVanillaCollections()) { + return map; + } + return this; + } + + public void internalAdd(Object bean) { + throw new RuntimeException("Not allowed for map"); + } + + /** + * Return true if the underlying map has been populated. Returns false if it + * has a deferred fetch pending. + */ + public boolean isPopulated() { + return map != null; + } + + /** + * Return true if this is a reference (lazy loading) bean collection. This is + * the same as !isPopulated(); + */ + public boolean isReference() { + return map == null; + } + + public boolean checkEmptyLazyLoad() { + if (map == null) { + map = new LinkedHashMap(); + return true; + } else { + return false; + } + } + + private void initClear() { + synchronized (this) { + if (map == null) { + if (modifyListening) { + lazyLoadCollection(true); + } else { + map = new LinkedHashMap(); + } + } + touched(); + } + } + + private void init() { + synchronized (this) { + if (map == null) { + lazyLoadCollection(false); + } + touched(); + } + } + + /** + * Set the actual underlying map. Used for performing lazy fetch. + */ + @SuppressWarnings("unchecked") + public void setActualMap(Map map) { + this.map = (Map) map; + } + + /** + * Return the actual underlying map. + */ + public Map getActualMap() { + return map; + } + + /** + * Returns the map entrySet iterator. + *

    + * This is because the key values may need to be set against the details (so + * they don't need to be set twice). + *

    + */ + public Collection getActualDetails() { + return map.values(); + } + + /** + * Returns the underlying map. + */ + public Object getActualCollection() { + return map; + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("BeanMap "); + if (isReadOnly()) { + sb.append("readOnly "); + } + if (map == null) { + sb.append("deferred "); + + } else { + sb.append("size[").append(map.size()).append("]"); + sb.append(" hasMoreRows[").append(hasMoreRows).append("]"); + sb.append(" map").append(map); + } + return sb.toString(); + } + + /** + * Equal if obj is a Map and equal in a Map sense. + */ + public boolean equals(Object obj) { + init(); + return map.equals(obj); + } + + public int hashCode() { + init(); + return map.hashCode(); + } + + public void clear() { + checkReadOnly(); + initClear(); + if (modifyRemoveListening) { + for (K key : map.keySet()) { + E o = map.remove(key); + modifyRemoval(o); + } + } + map.clear(); + } + + public boolean containsKey(Object key) { + init(); + return map.containsKey(key); + } + + public boolean containsValue(Object value) { + init(); + return map.containsValue(value); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Set> entrySet() { + init(); + if (isReadOnly()) { + return Collections.unmodifiableSet(map.entrySet()); + } + if (modifyListening) { + Set> s = map.entrySet(); + return new ModifySet(this, s); + } + return map.entrySet(); + } + + public E get(Object key) { + init(); + return map.get(key); + } + + public boolean isEmpty() { + init(); + return map.isEmpty(); + } + + public Set keySet() { + init(); + if (isReadOnly()) { + return Collections.unmodifiableSet(map.keySet()); + } + // we don't really care about modifications to the ketSet? + return map.keySet(); + } + + public E put(K key, E value) { + checkReadOnly(); + init(); + if (modifyListening) { + Object o = map.put(key, value); + modifyAddition(value); + modifyRemoval(o); + } + return map.put(key, value); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void putAll(Map t) { + checkReadOnly(); + init(); + if (modifyListening) { + Iterator it = t.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = (Map.Entry) it.next(); + Object o = map.put((K) entry.getKey(), (E) entry.getValue()); + modifyAddition((E) entry.getValue()); + modifyRemoval(o); + } + } + map.putAll(t); + } + + public E remove(Object key) { + checkReadOnly(); + init(); + if (modifyRemoveListening) { + E o = map.remove(key); + modifyRemoval(o); + return o; + } + return map.remove(key); + } + + public int size() { + init(); + return map.size(); + } + + public Collection values() { + init(); + if (isReadOnly()) { + return Collections.unmodifiableCollection(map.values()); + } + if (modifyListening) { + Collection c = map.values(); + return new ModifyCollection(this, c); + } + return map.values(); + } + +} diff --git a/src/main/java/com/avaje/ebean/common/BeanSet.java b/src/main/java/com/avaje/ebean/common/BeanSet.java new file mode 100644 index 000000000..552a5c190 --- /dev/null +++ b/src/main/java/com/avaje/ebean/common/BeanSet.java @@ -0,0 +1,332 @@ +package com.avaje.ebean.common; + +import java.io.ObjectStreamException; +import java.io.Serializable; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; + +import com.avaje.ebean.bean.BeanCollectionAdd; +import com.avaje.ebean.bean.BeanCollectionLoader; +import com.avaje.ebean.bean.SerializeControl; + +/** + * Set capable of lazy loading. + */ +public final class BeanSet extends AbstractBeanCollection implements Set, + BeanCollectionAdd { + + /** + * The underlying Set implementation. + */ + private Set set; + + /** + * Create with a specific Set implementation. + */ + public BeanSet(Set set) { + this.set = set; + } + + /** + * Create using an underlying LinkedHashSet. + */ + public BeanSet() { + this(new LinkedHashSet()); + } + + public BeanSet(BeanCollectionLoader loader, Object ownerBean, String propertyName) { + super(loader, ownerBean, propertyName); + } + + Object readResolve() throws ObjectStreamException { + if (SerializeControl.isVanillaCollections()) { + return set; + } + return this; + } + + Object writeReplace() throws ObjectStreamException { + if (SerializeControl.isVanillaCollections()) { + return set; + } + return this; + } + + @SuppressWarnings("unchecked") + public void addBean(Object bean) { + set.add((E) bean); + } + + @SuppressWarnings("unchecked") + public void internalAdd(Object bean) { + set.add((E) bean); + } + + /** + * Returns true if the underlying set has its data. + */ + public boolean isPopulated() { + return set != null; + } + + /** + * Return true if this is a reference (lazy loading) bean collection. This is + * the same as !isPopulated(); + */ + public boolean isReference() { + return set == null; + } + + public boolean checkEmptyLazyLoad() { + if (set == null) { + set = new LinkedHashSet(); + return true; + } else { + return false; + } + } + + private void initClear() { + synchronized (this) { + if (set == null) { + if (modifyListening) { + lazyLoadCollection(true); + } else { + set = new LinkedHashSet(); + } + } + touched(); + } + } + + private void init() { + synchronized (this) { + if (set == null) { + lazyLoadCollection(true); + } + touched(); + } + } + + /** + * Set the underlying set (used for lazy fetch). + */ + @SuppressWarnings("unchecked") + public void setActualSet(Set set) { + this.set = (Set) set; + } + + /** + * Return the actual underlying set. + */ + public Set getActualSet() { + return set; + } + + public Collection getActualDetails() { + return set; + } + + /** + * Returns the underlying set. + */ + public Object getActualCollection() { + return set; + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("BeanSet "); + if (isReadOnly()) { + sb.append("readOnly "); + } + if (set == null) { + sb.append("deferred "); + + } else { + sb.append("size[").append(set.size()).append("]"); + sb.append(" hasMoreRows[").append(hasMoreRows).append("]"); + sb.append(" set").append(set); + } + return sb.toString(); + } + + /** + * Equal if obj is a Set and equal in a Set sense. + */ + public boolean equals(Object obj) { + init(); + return set.equals(obj); + } + + public int hashCode() { + init(); + return set.hashCode(); + } + + // -----------------------------------------------------// + // proxy method for map + // -----------------------------------------------------// + + public boolean add(E o) { + checkReadOnly(); + init(); + if (modifyAddListening) { + if (set.add(o)) { + modifyAddition(o); + return true; + } else { + return false; + } + } + return set.add(o); + } + + public boolean addAll(Collection c) { + checkReadOnly(); + init(); + if (modifyAddListening) { + boolean changed = false; + Iterator it = c.iterator(); + while (it.hasNext()) { + E o = it.next(); + if (set.add(o)) { + modifyAddition(o); + changed = true; + } + } + return changed; + } + return set.addAll(c); + } + + public void clear() { + checkReadOnly(); + initClear(); + if (modifyRemoveListening) { + Iterator it = set.iterator(); + while (it.hasNext()) { + E e = it.next(); + modifyRemoval(e); + } + } + set.clear(); + } + + public boolean contains(Object o) { + init(); + return set.contains(o); + } + + public boolean containsAll(Collection c) { + init(); + return set.containsAll(c); + } + + public boolean isEmpty() { + init(); + return set.isEmpty(); + } + + public Iterator iterator() { + init(); + if (isReadOnly()) { + return new ReadOnlyIterator(set.iterator()); + } + if (modifyListening) { + return new ModifyIterator(this, set.iterator()); + } + return set.iterator(); + } + + public boolean remove(Object o) { + checkReadOnly(); + init(); + if (modifyRemoveListening) { + if (set.remove(o)) { + modifyRemoval(o); + return true; + } + return false; + } + return set.remove(o); + } + + public boolean removeAll(Collection c) { + checkReadOnly(); + init(); + if (modifyRemoveListening) { + boolean changed = false; + Iterator it = c.iterator(); + while (it.hasNext()) { + Object o = (Object) it.next(); + if (set.remove(o)) { + modifyRemoval(o); + changed = true; + } + } + return changed; + } + return set.removeAll(c); + } + + public boolean retainAll(Collection c) { + checkReadOnly(); + init(); + if (modifyRemoveListening) { + boolean changed = false; + Iterator it = set.iterator(); + while (it.hasNext()) { + Object o = it.next(); + if (!c.contains(o)) { + it.remove(); + modifyRemoval(o); + changed = true; + } + } + return changed; + } + return set.retainAll(c); + } + + public int size() { + init(); + return set.size(); + } + + public Object[] toArray() { + init(); + return set.toArray(); + } + + public T[] toArray(T[] a) { + init(); + return set.toArray(a); + } + + private static class ReadOnlyIterator implements Iterator, Serializable { + + private static final long serialVersionUID = 2577697326745352605L; + + private final Iterator it; + + ReadOnlyIterator(Iterator it) { + this.it = it; + } + + public boolean hasNext() { + return it.hasNext(); + } + + public E next() { + return it.next(); + } + + public void remove() { + throw new IllegalStateException("This collection is in ReadOnly mode"); + } + } + +} diff --git a/src/main/java/com/avaje/ebean/common/BootupEbeanManager.java b/src/main/java/com/avaje/ebean/common/BootupEbeanManager.java new file mode 100644 index 000000000..60535ac4f --- /dev/null +++ b/src/main/java/com/avaje/ebean/common/BootupEbeanManager.java @@ -0,0 +1,38 @@ +package com.avaje.ebean.common; + +import com.avaje.ebean.EbeanServer; +import com.avaje.ebean.config.ServerConfig; + +/** + * Creates the EbeanServer implementations. This is used by the Ebean singleton + * to determine the implementation for each server name. + *

    + * Note that on a remote client it is expected that this factory will return + * EbeanServers that behave as a proxy using http or tcp sockets etc to talk to + * the EbeanServer on the application server. + *

    + */ +public interface BootupEbeanManager { + + /** + * Create the EbeanServer for a given configuration. + * + * @param configuration + * The configuration information for this server. + */ + public EbeanServer createServer(ServerConfig configuration); + + /** + * Create an EbeanServer just using the name. + *

    + * In this case the dataSource parameters etc will be defined on the global + * avaje.properties file. + *

    + */ + public EbeanServer createServer(String name); + + /** + * Shutdown any Ebean wide resources such as clustering. + */ + public void shutdown(); +} diff --git a/src/main/java/com/avaje/ebean/common/ModifyCollection.java b/src/main/java/com/avaje/ebean/common/ModifyCollection.java new file mode 100644 index 000000000..22b523256 --- /dev/null +++ b/src/main/java/com/avaje/ebean/common/ModifyCollection.java @@ -0,0 +1,122 @@ +package com.avaje.ebean.common; + +import java.util.Collection; +import java.util.Iterator; + +import com.avaje.ebean.bean.BeanCollection; + +/** + * Wraps a collection for the purposes of notifying removals and additions to + * the BeanCollection owner. + *

    + * This is required for persisting ManyToMany objects. Additions and removals + * become inserts and deletes to the intersection table. + *

    + */ +class ModifyCollection implements Collection { + + protected final BeanCollection owner; + + protected final Collection c; + + /** + * Create with an Owner and the underlying collection this wraps. + *

    + * The owner is notified of the additions and removals. + *

    + */ + public ModifyCollection(BeanCollection owner, Collection c) { + this.owner = owner; + this.c = c; + } + + public boolean add(E o) { + if (c.add(o)) { + owner.modifyAddition(o); + return true; + } + return false; + } + + public boolean addAll(Collection collection) { + boolean changed = false; + Iterator it = collection.iterator(); + while (it.hasNext()) { + E o = it.next(); + if (c.add(o)) { + owner.modifyAddition(o); + changed = true; + } + } + return changed; + } + + public void clear() { + c.clear(); + } + + public boolean contains(Object o) { + return c.contains(o); + } + + public boolean containsAll(Collection collection) { + return c.containsAll(collection); + } + + public boolean isEmpty() { + return c.isEmpty(); + } + + public Iterator iterator() { + Iterator it = c.iterator(); + return new ModifyIterator(owner, it); + } + + public boolean remove(Object o) { + if (c.remove(o)) { + owner.modifyRemoval(o); + return true; + } + return false; + } + + public boolean removeAll(Collection collection) { + boolean changed = false; + Iterator it = collection.iterator(); + while (it.hasNext()) { + Object o = (Object) it.next(); + if (c.remove(o)) { + owner.modifyRemoval(o); + changed = true; + } + } + return changed; + } + + public boolean retainAll(Collection collection) { + boolean changed = false; + Iterator it = c.iterator(); + while (it.hasNext()) { + Object o = (Object) it.next(); + if (!collection.contains(o)) { + it.remove(); + owner.modifyRemoval(o); + changed = true; + } + } + return changed; + } + + public int size() { + return c.size(); + } + + public Object[] toArray() { + return c.toArray(); + } + + public T[] toArray(T[] a) { + return c.toArray(a); + } + +} diff --git a/src/main/java/com/avaje/ebean/common/ModifyHolder.java b/src/main/java/com/avaje/ebean/common/ModifyHolder.java new file mode 100644 index 000000000..2db4dfe0f --- /dev/null +++ b/src/main/java/com/avaje/ebean/common/ModifyHolder.java @@ -0,0 +1,74 @@ +package com.avaje.ebean.common; + +import java.io.Serializable; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Holds sets of additions and deletions from a 'owner' List Set or Map. + *

    + * These sets of additions and deletions are used to support persisting + * ManyToMany relationships. The additions becoming inserts into the + * intersection table and the removals becoming deletes from the intersection + * table. + *

    + */ +class ModifyHolder implements Serializable { + + private static final long serialVersionUID = 2572572897923801083L; + + /** + * Deletions list for manyToMany persistence. + */ + private Set modifyDeletions = new LinkedHashSet(); + + /** + * Additions list for manyToMany persistence. + */ + private Set modifyAdditions = new LinkedHashSet(); + + void reset() { + modifyDeletions = new LinkedHashSet(); + modifyAdditions = new LinkedHashSet(); + } + + /** + * Used by BeanList.addAll() methods. + */ + void modifyAdditionAll(Collection c) { + if (c != null) { + for (E e : c) { + modifyAddition(e); + } + } + } + + void modifyAddition(E bean) { + if (bean != null) { + // If it is to delete then just remove the deletion + if (!modifyDeletions.remove(bean)) { + // Insert + modifyAdditions.add(bean); + } + } + } + + @SuppressWarnings("unchecked") + void modifyRemoval(Object bean) { + if (bean != null) { + // If it is to be added then just remove the addition + if (!modifyAdditions.remove((E) bean)) { + modifyDeletions.add((E) bean); + } + } + } + + Set getModifyAdditions() { + return modifyAdditions; + } + + Set getModifyRemovals() { + return modifyDeletions; + } +} diff --git a/src/main/java/com/avaje/ebean/common/ModifyIterator.java b/src/main/java/com/avaje/ebean/common/ModifyIterator.java new file mode 100644 index 000000000..d17c1eb14 --- /dev/null +++ b/src/main/java/com/avaje/ebean/common/ModifyIterator.java @@ -0,0 +1,48 @@ +package com.avaje.ebean.common; + +import java.util.Iterator; + +import com.avaje.ebean.bean.BeanCollection; + +/** + * Wraps an iterator for the purposes of notifying removals and additions to the + * BeanCollection owner. + *

    + * This is required for persisting ManyToMany objects. Additions and removals + * become inserts and deletes to the intersection table. + *

    + */ +class ModifyIterator implements Iterator { + + private final BeanCollection owner; + + private final Iterator it; + + private E last; + + /** + * Create with an Owner and the underlying Iterator this wraps. + *

    + * The owner is notified of the removals. + *

    + */ + ModifyIterator(BeanCollection owner, Iterator it) { + this.owner = owner; + this.it = it; + } + + public boolean hasNext() { + return it.hasNext(); + } + + public E next() { + last = it.next(); + return last; + } + + public void remove() { + owner.modifyRemoval(last); + it.remove(); + } + +} diff --git a/src/main/java/com/avaje/ebean/common/ModifyList.java b/src/main/java/com/avaje/ebean/common/ModifyList.java new file mode 100644 index 000000000..0944db16c --- /dev/null +++ b/src/main/java/com/avaje/ebean/common/ModifyList.java @@ -0,0 +1,91 @@ +package com.avaje.ebean.common; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import com.avaje.ebean.bean.BeanCollection; + +/** + * Wraps a List for the purposes of notifying removals and additions to the + * BeanCollection owner. + *

    + * This is required for persisting ManyToMany objects. Additions and removals + * become inserts and deletes to the intersection table. + *

    + *

    + * Note that this is created by a call to subList() on a BeanList. Thats its + * only purpose really. BeanList holds the actual List. + *

    + */ +class ModifyList extends ModifyCollection implements List { + + /** + * The underlying list. + */ + private final List list; + + /** + * Create with an Owner that is notified of any additions or deletions. + */ + ModifyList(BeanCollection owner, List list) { + super(owner, list); + this.list = list; + } + + public void add(int index, E element) { + list.add(index, element); + owner.modifyAddition(element); + } + + public boolean addAll(int index, Collection co) { + if (list.addAll(index, co)) { + Iterator it = co.iterator(); + while (it.hasNext()) { + E o = it.next(); + owner.modifyAddition(o); + } + return true; + } + return false; + } + + public E get(int index) { + return list.get(index); + } + + public int indexOf(Object o) { + return list.indexOf(o); + } + + public int lastIndexOf(Object o) { + return list.lastIndexOf(o); + } + + public ListIterator listIterator() { + return new ModifyListIterator(owner, list.listIterator()); + } + + public ListIterator listIterator(int index) { + return new ModifyListIterator(owner, list.listIterator(index)); + } + + public E remove(int index) { + E o = list.remove(index); + owner.modifyRemoval(o); + return o; + } + + public E set(int index, E element) { + E o = list.set(index, element); + owner.modifyAddition(element); + owner.modifyRemoval(o); + return o; + } + + public List subList(int fromIndex, int toIndex) { + return new ModifyList(owner, list.subList(fromIndex, toIndex)); + } + +} diff --git a/src/main/java/com/avaje/ebean/common/ModifyListIterator.java b/src/main/java/com/avaje/ebean/common/ModifyListIterator.java new file mode 100644 index 000000000..0b649b774 --- /dev/null +++ b/src/main/java/com/avaje/ebean/common/ModifyListIterator.java @@ -0,0 +1,79 @@ +package com.avaje.ebean.common; + +import java.util.ListIterator; + +import com.avaje.ebean.bean.BeanCollection; + +/** + * Wraps a ListIterator for the purposes of notifying removals and additions to + * the BeanCollection owner. + *

    + * This is required for persisting ManyToMany objects. Additions and removals + * become inserts and deletes to the intersection table. + *

    + */ +class ModifyListIterator implements ListIterator { + + private final BeanCollection owner; + + private final ListIterator it; + + private E last; + + /** + * Create with an Owner that is notified of any additions or deletions. + */ + ModifyListIterator(BeanCollection owner, ListIterator it) { + this.owner = owner; + this.it = it; + } + + public void add(E bean) { + owner.modifyAddition(bean); + last = null; + it.add(bean); + } + + public boolean hasNext() { + return it.hasNext(); + } + + public boolean hasPrevious() { + return it.hasPrevious(); + } + + public E next() { + last = it.next(); + return last; + } + + public int nextIndex() { + return it.nextIndex(); + } + + public E previous() { + last = it.previous(); + return last; + } + + public int previousIndex() { + return it.previousIndex(); + } + + public void remove() { + owner.modifyRemoval(last); + last = null; + it.remove(); + } + + public void set(E o) { + if (last == null) { + // in theory this is not allowed + } else { + owner.modifyRemoval(last); + owner.modifyAddition(o); + } + it.set(o); + } + +} diff --git a/src/main/java/com/avaje/ebean/common/ModifySet.java b/src/main/java/com/avaje/ebean/common/ModifySet.java new file mode 100644 index 000000000..70eaa6598 --- /dev/null +++ b/src/main/java/com/avaje/ebean/common/ModifySet.java @@ -0,0 +1,24 @@ +package com.avaje.ebean.common; + +import java.util.Set; + +import com.avaje.ebean.bean.BeanCollection; + +/** + * Wraps a Set for the purposes of notifying removals and additions to the + * BeanCollection owner. + *

    + * This is required for persisting ManyToMany objects. Additions and removals + * become inserts and deletes to the intersection table. + *

    + */ +class ModifySet extends ModifyCollection implements Set { + + /** + * Create with an Owner that is notified of any additions or deletions. + */ + public ModifySet(BeanCollection owner, Set s) { + super(owner, s); + } + +} diff --git a/src/main/java/com/avaje/ebean/common/package.html b/src/main/java/com/avaje/ebean/common/package.html new file mode 100644 index 000000000..21956c462 --- /dev/null +++ b/src/main/java/com/avaje/ebean/common/package.html @@ -0,0 +1,11 @@ + + + + Common non-public interfaces and implementation + + +Common non-public interfaces and implementation. + + + + \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/config/AbstractNamingConvention.java b/src/main/java/com/avaje/ebean/config/AbstractNamingConvention.java new file mode 100644 index 000000000..1ade40b34 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/AbstractNamingConvention.java @@ -0,0 +1,282 @@ +package com.avaje.ebean.config; + +import javax.persistence.Inheritance; +import javax.persistence.Table; + +import com.avaje.ebean.config.dbplatform.DatabasePlatform; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides some base implementation for NamingConventions. + * + * @author emcgreal + */ +public abstract class AbstractNamingConvention implements NamingConvention { + + /** The Constant logger. */ + private static final Logger logger = LoggerFactory.getLogger(AbstractNamingConvention.class); + + /** The Constant DEFAULT_SEQ_FORMAT. */ + public static final String DEFAULT_SEQ_FORMAT = "{table}_seq"; + + /** Sequence Format that includes the Primary Key column */ + public static final String TABLE_PKCOLUMN_SEQ_FORMAT = "{table}_{column}_seq"; + + /** The catalog. */ + private String catalog; + + /** The schema. */ + private String schema; + + /** The sequence format. */ + private String sequenceFormat; + + /** The database platform. */ + protected DatabasePlatform databasePlatform; + + /** The max length of constraint names. */ + protected int maxConstraintNameLength; + + /** Used to trim off extra prefix for M2M. */ + protected int rhsPrefixLength = 3; + + protected boolean useForeignKeyPrefix = true; + + /** + * Construct with a sequence format and useForeignKeyPrefix setting. + */ + public AbstractNamingConvention(String sequenceFormat, boolean useForeignKeyPrefix) { + this.sequenceFormat = sequenceFormat; + this.useForeignKeyPrefix = useForeignKeyPrefix; + } + + /** + * Construct with a sequence format. + * + * @param sequenceFormat + * the sequence format + */ + public AbstractNamingConvention(String sequenceFormat) { + this.sequenceFormat = sequenceFormat; + } + + /** + * Construct with the default sequence format ("{table}_seq") and + * useForeignKeyPrefix as true. + */ + public AbstractNamingConvention() { + this(DEFAULT_SEQ_FORMAT); + } + + public void setDatabasePlatform(DatabasePlatform databasePlatform) { + this.databasePlatform = databasePlatform; + this.maxConstraintNameLength = databasePlatform.getDbDdlSyntax().getMaxConstraintNameLength(); + + logger.trace("Using maxConstraintNameLength of " + maxConstraintNameLength); + } + + public String getSequenceName(String tableName, String pkColumn) { + String s = sequenceFormat.replace("{table}", tableName); + if (pkColumn == null) { + pkColumn = ""; + } + return s.replace("{column}", pkColumn); + } + + /** + * Return the catalog. + */ + public String getCatalog() { + return catalog; + } + + /** + * Sets the catalog. + */ + public void setCatalog(String catalog) { + this.catalog = catalog; + } + + /** + * Return the schema. + */ + public String getSchema() { + return schema; + } + + /** + * Sets the schema. + */ + public void setSchema(String schema) { + this.schema = schema; + } + + /** + * Returns the sequence format. + */ + public String getSequenceFormat() { + return sequenceFormat; + } + + /** + * Set the sequence format used to generate the sequence name. + *

    + * The format should include "{table}". When generating the sequence name + * {table} is replaced with the actual table name. + *

    + * + * @param sequenceFormat + * string containing "{table}" which is replaced with the actual + * table name to generate the sequence name. + */ + public void setSequenceFormat(String sequenceFormat) { + this.sequenceFormat = sequenceFormat; + } + + /** + * Return true if a prefix should be used building a foreign key name. + *

    + * This by default is true and this works well when the primary key column + * names are simply "ID". In this case a prefix (such as "ORDER" and + * "CUSTOMER" etc) is added to the foreign key column producing "ORDER_ID" and + * "CUSTOMER_ID". + *

    + *

    + * This should return false when your primary key columns are the same as the + * foreign key columns. For example, when the primary key columns are + * "ORDER_ID", "CUST_ID" etc ... and they are the same as the foreign key + * column names. + *

    + */ + public boolean isUseForeignKeyPrefix() { + return useForeignKeyPrefix; + } + + /** + * Set this to false when the primary key columns matching your foreign key + * columns. + */ + public void setUseForeignKeyPrefix(boolean useForeignKeyPrefix) { + this.useForeignKeyPrefix = useForeignKeyPrefix; + } + + /** + * Return the tableName using the naming convention (rather than deployed + * Table annotation). + */ + protected abstract TableName getTableNameByConvention(Class beanClass); + + /** + * Returns the table name for a given entity bean. + *

    + * This first checks for the @Table annotation and if not present uses the + * naming convention to define the table name. + *

    + * + * @see #getTableNameFromAnnotation(Class) + * @see #getTableNameByConvention(Class) + */ + public TableName getTableName(Class beanClass) { + + TableName tableName = getTableNameFromAnnotation(beanClass); + if (tableName == null) { + + Class supCls = beanClass.getSuperclass(); + Inheritance inheritance = supCls.getAnnotation(Inheritance.class); + if (inheritance != null) { + // get the table as per inherited class in case their + // is not a table annotation in the inheritance hierarchy + return getTableName(supCls); + } + + tableName = getTableNameByConvention(beanClass); + } + + // Use naming convention for catalog or schema, + // if not set in the annotation. + String catalog = tableName.getCatalog(); + if (isEmpty(catalog)) { + catalog = getCatalog(); + } + String schema = tableName.getSchema(); + if (isEmpty(schema)) { + schema = getSchema(); + } + return new TableName(catalog, schema, tableName.getName()); + } + + public TableName getM2MJoinTableName(TableName lhsTable, TableName rhsTable) { + + StringBuilder buffer = new StringBuilder(); + buffer.append(lhsTable.getName()); + buffer.append("_"); + + String rhsTableName = rhsTable.getName(); + if (rhsTableName.indexOf('_') < rhsPrefixLength) { + // trim off a xx_ prefix if there is one + rhsTableName = rhsTableName.substring(rhsTableName.indexOf('_') + 1); + } + buffer.append(rhsTableName); + + int maxConstraintNameLength = databasePlatform.getDbDdlSyntax().getMaxConstraintNameLength(); + + // maxConstraintNameLength is used as the max table name length. + if (buffer.length() > maxConstraintNameLength) { + buffer.setLength(maxConstraintNameLength); + } + + return new TableName(lhsTable.getCatalog(), lhsTable.getSchema(), buffer.toString()); + } + + /** + * Gets the table name from annotation. + */ + protected TableName getTableNameFromAnnotation(Class beanClass) { + + final Table t = findTableAnnotation(beanClass); + + // Take the annotation if defined + if (t != null && !isEmpty(t.name())) { + // Note: empty catalog and schema are converted to null + // Only need to convert quoted identifiers from annotations + return new TableName(quoteIdentifiers(t.catalog()), quoteIdentifiers(t.schema()), + quoteIdentifiers(t.name())); + } + + // No annotation + return null; + } + + /** + * Search recursively for an @Table in the class hierarchy. + */ + protected Table findTableAnnotation(Class cls) { + if (cls.equals(Object.class)) { + return null; + } + Table table = cls.getAnnotation(Table.class); + if (table != null) { + return table; + } + return findTableAnnotation(cls.getSuperclass()); + } + + /** + * Replace back ticks (if they are used) with database platform specific + * quoted identifiers. + */ + protected String quoteIdentifiers(String s) { + return databasePlatform.convertQuotedIdentifiers(s); + } + + /** + * Checks string is null or empty . + */ + protected boolean isEmpty(String s) { + if (s == null || s.trim().length() == 0) { + return true; + } + return false; + } +} diff --git a/src/main/java/com/avaje/ebean/config/AutofetchConfig.java b/src/main/java/com/avaje/ebean/config/AutofetchConfig.java new file mode 100644 index 000000000..b9b8addbc --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/AutofetchConfig.java @@ -0,0 +1,231 @@ +package com.avaje.ebean.config; + +/** + * Defines the Autofetch behaviour for a EbeanServer. + */ +public class AutofetchConfig { + + private AutofetchMode mode = AutofetchMode.DEFAULT_ONIFEMPTY; + + private boolean queryTuning = false; + + private boolean queryTuningAddVersion = false; + + private boolean profiling = false; + + private int profilingMin = 1; + + private int profilingBase = 10; + + private double profilingRate = 0.05; + + private String logDirectory; + + private int profileUpdateFrequency = 60; + + private int garbageCollectionWait = 100; + + public AutofetchConfig() { + } + + /** + * Return the mode used when autofetch has not been explicit defined on a + * query. + */ + public AutofetchMode getMode() { + return mode; + } + + /** + * Set the mode used when autofetch has not been explicit defined on a query. + */ + public void setMode(AutofetchMode mode) { + this.mode = mode; + } + + /** + * Return true if the queries are being tuned. + */ + public boolean isQueryTuning() { + return queryTuning; + } + + /** + * Set to true if the queries should be tuned by autofetch. + */ + public void setQueryTuning(boolean queryTuning) { + this.queryTuning = queryTuning; + } + + /** + * Return true if the version property should be added when the query is + * tuned. + *

    + * If this is false then the version property will be added when profiling + * detects that the bean is possibly going to be modified. + *

    + */ + public boolean isQueryTuningAddVersion() { + return queryTuningAddVersion; + } + + /** + * Set to true to force the version property to be always added by the query + * tuning. + *

    + * If this is false then the version property will be added when profiling + * detects that the bean is possibly going to be modified. + *

    + */ + public void setQueryTuningAddVersion(boolean queryTuningAddVersion) { + this.queryTuningAddVersion = queryTuningAddVersion; + } + + /** + * Return true if profiling information should be collected. + */ + public boolean isProfiling() { + return profiling; + } + + /** + * Set to true if profiling information should be collected. + *

    + * The profiling information is collected and then used to generate the tuned + * queries for autofetch. + *

    + */ + public void setProfiling(boolean profiling) { + this.profiling = profiling; + } + + /** + * Return the minimum number of queries to profile before autofetch will start + * tuning the queries. + */ + public int getProfilingMin() { + return profilingMin; + } + + /** + * Set the minimum number of queries to profile before autofetch will start + * tuning the queries. + */ + public void setProfilingMin(int profilingMin) { + this.profilingMin = profilingMin; + } + + /** + * Return the base number of queries to profile before changing to profile + * only a percentage of following queries (profileRate). + */ + public int getProfilingBase() { + return profilingBase; + } + + /** + * Set the based number of queries to profile. + */ + public void setProfilingBase(int profilingBase) { + this.profilingBase = profilingBase; + } + + /** + * Return the rate (%) of queries to be profiled after the 'base' amount of + * profiling. + */ + public double getProfilingRate() { + return profilingRate; + } + + /** + * Set the rate (%) of queries to be profiled after the 'base' amount of + * profiling. + */ + public void setProfilingRate(double profilingRate) { + this.profilingRate = profilingRate; + } + + /** + * Return the log directory to put the autofetch log. + */ + public String getLogDirectory() { + return logDirectory; + } + + /** + * Return the log directory substituting any expressions such as + * ${catalina.base} etc. + */ + public String getLogDirectoryWithEval() { + return GlobalProperties.evaluateExpressions(logDirectory); + } + + /** + * Set the directory to put the autofetch log in. + */ + public void setLogDirectory(String logDirectory) { + this.logDirectory = logDirectory; + } + + /** + * Return the frequency in seconds to update the autofetch tuned queries from + * the profiled information. + */ + public int getProfileUpdateFrequency() { + return profileUpdateFrequency; + } + + /** + * Set the frequency in seconds to update the autofetch tuned queries from the + * profiled information. + */ + public void setProfileUpdateFrequency(int profileUpdateFrequency) { + this.profileUpdateFrequency = profileUpdateFrequency; + } + + /** + * Return the time in millis to wait after a system gc to collect profiling + * information. + *

    + * The profiling information is collected on object finalise. As such we + * generally don't want to trigger GC (let the JVM do its thing) but on + * shutdown the autofetch manager will trigger System.gc() and then wait + * (default 100 millis) to hopefully collect profiling information - + * especially for short run unit tests. + *

    + */ + public int getGarbageCollectionWait() { + return garbageCollectionWait; + } + + /** + * Set the time in millis to wait after a System.gc() to collect profiling + * information. + */ + public void setGarbageCollectionWait(int garbageCollectionWait) { + this.garbageCollectionWait = garbageCollectionWait; + } + + /** + * Load the settings from the properties file. + */ + public void loadSettings(GlobalProperties.PropertySource p) { + + logDirectory = p.get("autofetch.logDirectory", null); + queryTuning = p.getBoolean("autofetch.querytuning", false); + queryTuningAddVersion = p.getBoolean("autofetch.queryTuningAddVersion", false); + + profiling = p.getBoolean("autofetch.profiling", false); + mode = p + .getEnum(AutofetchMode.class, "autofetch.implicitmode", AutofetchMode.DEFAULT_ONIFEMPTY); + + profilingMin = p.getInt("autofetch.profiling.min", 1); + profilingBase = p.getInt("autofetch.profiling.base", 10); + + String rate = p.get("autofetch.profiling.rate", "0.05"); + profilingRate = Double.parseDouble(rate); + + profileUpdateFrequency = p.getInt("autofetch.profiling.updatefrequency", 60); + } +} diff --git a/src/main/java/com/avaje/ebean/config/AutofetchMode.java b/src/main/java/com/avaje/ebean/config/AutofetchMode.java new file mode 100644 index 000000000..86dcf95db --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/AutofetchMode.java @@ -0,0 +1,32 @@ +package com.avaje.ebean.config; + +import com.avaje.ebean.Query; + +/** + * The mode for determining if Autofetch will be used for a given query when + * {@link Query#setAutofetch(boolean)} has not been explicitly set on a query. + *

    + * The explicit control of {@link Query#setAutofetch(boolean)} will always take + * precedence. This mode is used when this has not been explicitly set on a + * query. + *

    + */ +public enum AutofetchMode { + + /** + * Don't implicitly use Autofetch. Must explicitly turn it on. + */ + DEFAULT_OFF, + + /** + * Use Autofetch implicitly. Must explicitly turn it off. + */ + DEFAULT_ON, + + /** + * Implicitly use Autofetch if the query has not got either select() or join() + * defined. + */ + DEFAULT_ONIFEMPTY + +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/config/CompoundType.java b/src/main/java/com/avaje/ebean/config/CompoundType.java new file mode 100644 index 000000000..8d79a46f7 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/CompoundType.java @@ -0,0 +1,46 @@ +package com.avaje.ebean.config; + +/** + * API from creating and getting property values from an Immutable Compound + * Value Object. + * + *

    + * A Compound Value object should contain multiple properties that are stored + * separately. If you only have a single scalar value you should instead look to + * use {@link ScalarTypeConverter}. + *

    + *

    + * For each property in the compound type you need to implement the + * {@link CompoundTypeProperty} interface. These must be returned from + * {@link #getProperties()} in the same order that the properties appear in the + * constructor. + *

    + *

    + * If your compound type is mutable then you should look to use the JPA Embedded + * annotation instead of implementing this interface. + *

    + *

    + * When using classpath search Ebean will detect and automatically register any + * implementations of this interface (along with detecting the entity classes + * etc). + *

    + * + * @author rbygrave + * + * @param + * The type of the Value Object + * + * @see ScalarTypeConverter + */ +public interface CompoundType { + + /** + * Create an instance of the compound type given its property values. + */ + public V create(Object[] propertyValues); + + /** + * Return the properties in the order they appear in the constructor. + */ + public CompoundTypeProperty[] getProperties(); +} diff --git a/src/main/java/com/avaje/ebean/config/CompoundTypeProperty.java b/src/main/java/com/avaje/ebean/config/CompoundTypeProperty.java new file mode 100644 index 000000000..1874649a8 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/CompoundTypeProperty.java @@ -0,0 +1,51 @@ +package com.avaje.ebean.config; + +/** + * Represents a Property of a Compound Value Object. + *

    + * For each property in a {@link CompoundType} you need an implementation of + * this CompoundTypeProperty interface. + * + *

    + * + * @author rbygrave + * + * @param + * The type of the Compound value object + * @param

    + * The type of the property + * + * @see CompoundType + * @see ScalarTypeConverter + */ +public interface CompoundTypeProperty { + + /** + * The name of this property. + */ + public String getName(); + + /** + * Return the property value from the containing compound value object. + * + * @param valueObject + * the compound value object + * @return the property value. + */ + public P getValue(V valueObject); + + /** + * This should ONLY be used when the persistence type is different from + * the logical type returned. It most cases just return 0 and Ebean will + * persist the logical type. + *

    + * Typically this should be used when the logical type is long but the + * persistence type is java.sql.Timestamp. In this case return + * java.sql.Types.TIMESTAMP (rather than 0). + *

    + * + * @return Return the java.sql.Type that you want to use to persist this + * property or 0 and Ebean will use the logical type. + */ + public int getDbType(); +} diff --git a/src/main/java/com/avaje/ebean/config/ConfigPropertyMap.java b/src/main/java/com/avaje/ebean/config/ConfigPropertyMap.java new file mode 100644 index 000000000..680794979 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/ConfigPropertyMap.java @@ -0,0 +1,50 @@ +package com.avaje.ebean.config; + +import com.avaje.ebean.config.GlobalProperties.PropertySource; + +/** + * Helper to read server specific properties from ebean.properties. + */ +class ConfigPropertyMap implements PropertySource { + + private final String serverName; + + public ConfigPropertyMap(String serverName) { + this.serverName = serverName; + } + + public String getServerName() { + return serverName; + } + + public String get(String key, String defaultValue) { + String namedKey = "ebean." + serverName + "." + key; + String inheritKey = "ebean." + key; + String value = GlobalProperties.get(namedKey, null); + if (value == null) { + value = GlobalProperties.get(inheritKey, null); + } + if (value == null) { + return defaultValue; + } else { + return value; + } + } + + public int getInt(String key, int defaultValue) { + + String value = get(key, String.valueOf(defaultValue)); + return Integer.parseInt(value); + } + + public boolean getBoolean(String key, boolean defaultValue) { + + String value = get(key, String.valueOf(defaultValue)); + return Boolean.parseBoolean(value); + } + + public > T getEnum(Class enumType, String key, T defaultValue) { + String level = get(key, defaultValue.name()); + return Enum.valueOf(enumType, level.toUpperCase()); + } +} diff --git a/src/main/java/com/avaje/ebean/config/DataSourceConfig.java b/src/main/java/com/avaje/ebean/config/DataSourceConfig.java new file mode 100644 index 000000000..8779a1fb1 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/DataSourceConfig.java @@ -0,0 +1,431 @@ +package com.avaje.ebean.config; + +import java.sql.Connection; +import java.util.Map; + +import com.avaje.ebean.Transaction; +import com.avaje.ebean.util.StringHelper; + +/** + * Used to config a DataSource when using the internal Ebean DataSource + * implementation. + *

    + * If a DataSource instance is already defined via + * {@link ServerConfig#setDataSource(javax.sql.DataSource)} or defined as JNDI + * dataSource via {@link ServerConfig#setDataSourceJndiName(String)} then those + * will used and not this DataSourceConfig. + *

    + */ +public class DataSourceConfig { + + private String url; + + private String username; + + private String password; + + private String driver; + + private int minConnections = 2; + + private int maxConnections = 20; + + private int isolationLevel = Transaction.READ_COMMITTED; + + private String heartbeatSql; + + private boolean captureStackTrace; + + private int maxStackTraceSize = 5; + + private int leakTimeMinutes = 30; + + private int maxInactiveTimeSecs = 900; + + private int pstmtCacheSize = 20; + private int cstmtCacheSize = 20; + + private int waitTimeoutMillis = 1000; + + private String poolListener; + + private boolean offline; + + Map customProperties; + + /** + * Return the connection URL. + */ + public String getUrl() { + return url; + } + + /** + * Set the connection URL. + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * Return the database username. + */ + public String getUsername() { + return username; + } + + /** + * Set the database username. + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * Return the database password. + */ + public String getPassword() { + return password; + } + + /** + * Set the database password. + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Return the database driver. + */ + public String getDriver() { + return driver; + } + + /** + * Set the database driver. + */ + public void setDriver(String driver) { + this.driver = driver; + } + + /** + * Return the transaction isolation level. + */ + public int getIsolationLevel() { + return isolationLevel; + } + + /** + * Set the transaction isolation level. + */ + public void setIsolationLevel(int isolationLevel) { + this.isolationLevel = isolationLevel; + } + + /** + * Return the minimum number of connections the pool should maintain. + */ + public int getMinConnections() { + return minConnections; + } + + /** + * Set the minimum number of connections the pool should maintain. + */ + public void setMinConnections(int minConnections) { + this.minConnections = minConnections; + } + + /** + * Return the maximum number of connections the pool can reach. + */ + public int getMaxConnections() { + return maxConnections; + } + + /** + * Set the maximum number of connections the pool can reach. + */ + public void setMaxConnections(int maxConnections) { + this.maxConnections = maxConnections; + } + + /** + * Return a SQL statement used to test the database is accessible. + *

    + * Note that if this is not set then it can get defaulted from the + * DatabasePlatform. + *

    + */ + public String getHeartbeatSql() { + return heartbeatSql; + } + + /** + * Set a SQL statement used to test the database is accessible. + *

    + * Note that if this is not set then it can get defaulted from the + * DatabasePlatform. + *

    + */ + public void setHeartbeatSql(String heartbeatSql) { + this.heartbeatSql = heartbeatSql; + } + + /** + * Return true if a stack trace should be captured when obtaining a connection + * from the pool. + *

    + * This can be used to diagnose a suspected connection pool leak. + *

    + *

    + * Obviously this has a performance overhead. + *

    + */ + public boolean isCaptureStackTrace() { + return captureStackTrace; + } + + /** + * Set to true if a stack trace should be captured when obtaining a connection + * from the pool. + *

    + * This can be used to diagnose a suspected connection pool leak. + *

    + *

    + * Obviously this has a performance overhead. + *

    + */ + public void setCaptureStackTrace(boolean captureStackTrace) { + this.captureStackTrace = captureStackTrace; + } + + /** + * Return the max size for reporting stack traces on busy connections. + */ + public int getMaxStackTraceSize() { + return maxStackTraceSize; + } + + /** + * Set the max size for reporting stack traces on busy connections. + */ + public void setMaxStackTraceSize(int maxStackTraceSize) { + this.maxStackTraceSize = maxStackTraceSize; + } + + /** + * Return the time in minutes after which a connection could be considered to + * have leaked. + */ + public int getLeakTimeMinutes() { + return leakTimeMinutes; + } + + /** + * Set the time in minutes after which a connection could be considered to + * have leaked. + */ + public void setLeakTimeMinutes(int leakTimeMinutes) { + this.leakTimeMinutes = leakTimeMinutes; + } + + /** + * Return the size of the PreparedStatement cache (per connection). + */ + public int getPstmtCacheSize() { + return pstmtCacheSize; + } + + /** + * Set the size of the PreparedStatement cache (per connection). + */ + public void setPstmtCacheSize(int pstmtCacheSize) { + this.pstmtCacheSize = pstmtCacheSize; + } + + /** + * Return the size of the CallableStatement cache (per connection). + */ + public int getCstmtCacheSize() { + return cstmtCacheSize; + } + + /** + * Set the size of the CallableStatement cache (per connection). + */ + public void setCstmtCacheSize(int cstmtCacheSize) { + this.cstmtCacheSize = cstmtCacheSize; + } + + /** + * Return the time in millis to wait for a connection before timing out once + * the pool has reached its maximum size. + */ + public int getWaitTimeoutMillis() { + return waitTimeoutMillis; + } + + /** + * Set the time in millis to wait for a connection before timing out once the + * pool has reached its maximum size. + */ + public void setWaitTimeoutMillis(int waitTimeoutMillis) { + this.waitTimeoutMillis = waitTimeoutMillis; + } + + /** + * Return the time in seconds a connection can be idle after which it can be + * trimmed from the pool. + *

    + * This is so that the pool after a busy period can trend over time back + * towards the minimum connections. + *

    + */ + public int getMaxInactiveTimeSecs() { + return maxInactiveTimeSecs; + } + + /** + * Set the time in seconds a connection can be idle after which it can be + * trimmed from the pool. + *

    + * This is so that the pool after a busy period can trend over time back + * towards the minimum connections. + *

    + */ + public void setMaxInactiveTimeSecs(int maxInactiveTimeSecs) { + this.maxInactiveTimeSecs = maxInactiveTimeSecs; + } + + /** + * Return the pool listener. + */ + public String getPoolListener() { + return poolListener; + } + + /** + * Set a pool listener. + */ + public void setPoolListener(String poolListener) { + this.poolListener = poolListener; + } + + /** + * Return true if the DataSource should be left offline. + *

    + * This is to support DDL generation etc without having a real database. + *

    + */ + public boolean isOffline() { + return offline; + } + + /** + * Set to true if the DataSource should be left offline. + *

    + * This is to support DDL generation etc without having a real database. + *

    + *

    + * Note that you MUST specify the database platform name (oracle, postgres, + * h2, mysql etc) using {@link ServerConfig#setDatabasePlatformName(String)} + * when you do this. + *

    + */ + public void setOffline(boolean offline) { + this.offline = offline; + } + + /** + * Return a map of custom properties for the jdbc driver connection. + */ + public Map getCustomProperties() { + return customProperties; + } + + /** + * Set custom properties for the jdbc driver connection. + * + * @param customProperties + */ + public void setCustomProperties(Map customProperties) { + this.customProperties = customProperties; + } + + public void loadSettings(String serverName) { + loadSettingsCustomPrefix("datasource." + serverName + ".", new GlobalProperties.DelegatedGlobalPropertySource(serverName)); + } + + /** + * Load the settings from ebean.properties. + */ + public void loadSettingsCustomPrefix(String prefix, GlobalProperties.PropertySource properties) { + + this.username = properties.get(prefix + "username", null); + this.password = properties.get(prefix + "password", null); + + String v; + + v = properties.get(prefix + "databaseDriver", null); + this.driver = properties.get(prefix + "driver", v); + + v = properties.get(prefix + "databaseUrl", null); + this.url = properties.get(prefix + "url", v); + + this.captureStackTrace = properties.getBoolean(prefix + "captureStackTrace", false); + this.maxStackTraceSize = properties.getInt(prefix + "maxStackTraceSize", 5); + this.leakTimeMinutes = properties.getInt(prefix + "leakTimeMinutes", 30); + this.maxInactiveTimeSecs = properties.getInt(prefix + "maxInactiveTimeSecs", 900); + + this.minConnections = properties.getInt(prefix + "minConnections", 0); + this.maxConnections = properties.getInt(prefix + "maxConnections", 20); + this.pstmtCacheSize = properties.getInt(prefix + "pstmtCacheSize", 20); + this.cstmtCacheSize = properties.getInt(prefix + "cstmtCacheSize", 20); + + this.waitTimeoutMillis = properties.getInt(prefix + "waitTimeout", 1000); + + this.heartbeatSql = properties.get(prefix + "heartbeatSql", null); + this.poolListener = properties.get(prefix + "poolListener", null); + this.offline = properties.getBoolean(prefix + "offline", false); + + String isoLevel = properties.get(prefix + "isolationlevel", "READ_COMMITTED"); + this.isolationLevel = getTransactionIsolationLevel(isoLevel); + + String customProperties = properties.get(prefix + "customProperties", null); + if (customProperties != null && customProperties.length() > 0) { + Map custProps = StringHelper.delimitedToMap(customProperties, ";", "="); + this.customProperties = custProps; + } + + } + + /** + * return the isolation level for a given string description. + */ + public int getTransactionIsolationLevel(String level) { + level = level.toUpperCase(); + if (level.startsWith("TRANSACTION")) { + level = level.substring("TRANSACTION".length()); + } + level = level.replace("_", ""); + if ("NONE".equalsIgnoreCase(level)) { + return Connection.TRANSACTION_NONE; + } + if ("READCOMMITTED".equalsIgnoreCase(level)) { + return Connection.TRANSACTION_READ_COMMITTED; + } + if ("READUNCOMMITTED".equalsIgnoreCase(level)) { + return Connection.TRANSACTION_READ_UNCOMMITTED; + } + if ("REPEATABLEREAD".equalsIgnoreCase(level)) { + return Connection.TRANSACTION_REPEATABLE_READ; + } + if ("SERIALIZABLE".equalsIgnoreCase(level)) { + return Connection.TRANSACTION_SERIALIZABLE; + } + + throw new RuntimeException("Transaction Isolaction level [" + level + "] is not known."); + } +} diff --git a/src/main/java/com/avaje/ebean/config/EncryptDeploy.java b/src/main/java/com/avaje/ebean/config/EncryptDeploy.java new file mode 100644 index 000000000..d87d0c5fe --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/EncryptDeploy.java @@ -0,0 +1,111 @@ +package com.avaje.ebean.config; + +/** + * Define the encryption options for a bean property. + *

    + * You can define the encryption options for a Bean property via the Encrypt + * annotation and programmatically via {@link EncryptDeployManager}. + *

    + * + * @author rbygrave + * + * @see EncryptDeployManager#getEncryptDeploy(TableName, String) + */ +public class EncryptDeploy { + + /** + * Use to define that no encryption should be used. + */ + public static final EncryptDeploy NO_ENCRYPT = new EncryptDeploy(Mode.MODE_NO_ENCRYPT, true, 0); + + /** + * Use to define that the Encrypt annotation should be used to control + * encryption. + */ + public static final EncryptDeploy ANNOTATION = new EncryptDeploy(Mode.MODE_ANNOTATION, true, 0); + + /** + * Use to define that Encryption should be used and String types should use DB + * encryption. + */ + public static final EncryptDeploy ENCRYPT_DB = new EncryptDeploy(Mode.MODE_ENCRYPT, true, 0); + + /** + * Use to define that Java client Encryption should be used (rather than DB + * encryption). + */ + public static final EncryptDeploy ENCRYPT_CLIENT = new EncryptDeploy(Mode.MODE_ENCRYPT, false, 0); + + /** + * The Encryption mode. + */ + public enum Mode { + /** + * Encrypt the property using DB encryption or Java client encryption + * depending on the type and dbEncryption flag. + */ + MODE_ENCRYPT, + + /** + * No encryption is used, even if there is an Encryption annotation on the + * property. + */ + MODE_NO_ENCRYPT, + + /** + * Use encryption options defined by the Encryption annotation on the + * property. If no annotation is on the property it is not encrypted. + */ + MODE_ANNOTATION + } + + private final Mode mode; + + private final boolean dbEncrypt; + + private final int dbLength; + + /** + * Construct with all options for Encryption including the dbLength. + * + * @param mode + * the Encryption mode + * @param dbEncrypt + * set to false if you want to use Java client side encryption rather + * than DB encryption. + * @param dbLength + * set the DB length to use. + */ + public EncryptDeploy(Mode mode, boolean dbEncrypt, int dbLength) { + this.mode = mode; + this.dbEncrypt = dbEncrypt; + this.dbLength = dbLength; + } + + /** + * Return the encryption mode. + */ + public Mode getMode() { + return mode; + } + + /** + * Return true if String type should use DB encryption. + *

    + * Return false if String type should use java client encryption instead. + *

    + */ + public boolean isDbEncrypt() { + return dbEncrypt; + } + + /** + * Return a hint to specify the DB length. + *

    + * Returning 0 means just use the normal DB length determination. + *

    + */ + public int getDbLength() { + return dbLength; + } +} diff --git a/src/main/java/com/avaje/ebean/config/EncryptDeployManager.java b/src/main/java/com/avaje/ebean/config/EncryptDeployManager.java new file mode 100644 index 000000000..c7ca0d08d --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/EncryptDeployManager.java @@ -0,0 +1,15 @@ +package com.avaje.ebean.config; + +/** + * Programmatically define which database columns are encrypted. + * + * @author rbygrave + * + */ +public interface EncryptDeployManager { + + /** + * Return true if the table column is encrypted. + */ + public EncryptDeploy getEncryptDeploy(TableName table, String column); +} diff --git a/src/main/java/com/avaje/ebean/config/EncryptKey.java b/src/main/java/com/avaje/ebean/config/EncryptKey.java new file mode 100644 index 000000000..1d17eb2dc --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/EncryptKey.java @@ -0,0 +1,18 @@ +package com.avaje.ebean.config; + +/** + * Represents the key used for encryption. + *

    + * For simple cases this often represent a simple String key but depending on + * the encryption method this could contain other details. + *

    + * + * @author rbygrave + */ +public interface EncryptKey { + + /** + * Return the string key value. + */ + public String getStringValue(); +} diff --git a/src/main/java/com/avaje/ebean/config/EncryptKeyManager.java b/src/main/java/com/avaje/ebean/config/EncryptKeyManager.java new file mode 100644 index 000000000..615db8d6c --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/EncryptKeyManager.java @@ -0,0 +1,23 @@ +package com.avaje.ebean.config; + +/** + * Determine keys used for encryption and decryption. + * + * @author rbygrave + */ +public interface EncryptKeyManager { + + /** + * Initialise the EncryptKeyManager. + *

    + * This gives the EncryptKeyManager the opportunity to get keys etc. + *

    + */ + public void initialise(); + + /** + * Return the key used to encrypt and decrypt a property mapping to the given + * table and column. + */ + public EncryptKey getEncryptKey(String tableName, String columnName); +} diff --git a/src/main/java/com/avaje/ebean/config/Encryptor.java b/src/main/java/com/avaje/ebean/config/Encryptor.java new file mode 100644 index 000000000..44491860b --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/Encryptor.java @@ -0,0 +1,34 @@ +package com.avaje.ebean.config; + +/** + * Used for Java side encryption of properties when DB encryption is not used. + *

    + * By default this is used on non-varchar types such as Blobs. + *

    + * + * @author rbygrave + * + */ +public interface Encryptor { + + /** + * Encrypt the data using the key. + */ + public byte[] encrypt(byte[] data, EncryptKey key); + + /** + * Decrypt the data using the key. + */ + public byte[] decrypt(byte[] data, EncryptKey key); + + /** + * Encrypt the formatted string value using a key. + */ + public byte[] encryptString(String formattedValue, EncryptKey key); + + /** + * Decrypt the data returning a formatted string value using a key. + */ + public String decryptString(byte[] data, EncryptKey key); + +} diff --git a/src/main/java/com/avaje/ebean/config/ExternalTransactionManager.java b/src/main/java/com/avaje/ebean/config/ExternalTransactionManager.java new file mode 100644 index 000000000..80f3ca02d --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/ExternalTransactionManager.java @@ -0,0 +1,21 @@ +package com.avaje.ebean.config; + +/** + * Provides awareness of externally managed transactions. + */ +public interface ExternalTransactionManager { + + /** + * Set the transaction manager. + *

    + * This will change when SPI is published but will do for now. + *

    + */ + public void setTransactionManager(Object transactionManager); + + /** + * Return the current transaction or null if there is none. + */ + public Object getCurrentTransaction(); + +} diff --git a/src/main/java/com/avaje/ebean/config/GlobalProperties.java b/src/main/java/com/avaje/ebean/config/GlobalProperties.java new file mode 100644 index 000000000..7e0ac7ec1 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/GlobalProperties.java @@ -0,0 +1,211 @@ +package com.avaje.ebean.config; + +import java.util.Map; +import java.util.Map.Entry; + +import javax.servlet.ServletContext; + +import com.avaje.ebean.util.ClassUtil; + +/** + * Provides access to properties loaded from the ebean.properties file. + */ +public final class GlobalProperties { + + private static volatile PropertyMap globalMap; + + private static boolean skipPrimaryServer; + + /** + * Set whether to skip automatically creating the primary server. + */ + public static synchronized void setSkipPrimaryServer(boolean skip) { + skipPrimaryServer = skip; + } + + /** + * Return true to skip automatically creating the primary server. + */ + public static synchronized boolean isSkipPrimaryServer() { + return skipPrimaryServer; + } + + /** + * Parse the string replacing any expressions like ${catalina.base}. + *

    + * This will evaluate expressions using first environment variables, than java + * system variables and lastly properties in ebean.properties - in that order. + *

    + *

    + * Expressions start with "${" and end with "}". + *

    + */ + public static String evaluateExpressions(String val) { + return getPropertyMap().eval(val); + } + + /** + * Parse and evaluate any expressions that have not already been evaluated. + */ + public static synchronized void evaluateExpressions() { + getPropertyMap().evaluateProperties(); + } + + /** + * In a servlet container environment this will additionally look in WEB-INF + * for the ebean.properties file. + */ + public static synchronized void setServletContext(ServletContext servletContext) { + + PropertyMapLoader.setServletContext(servletContext); + } + + /** + * Return the ServletContext (if setup in a servlet container environment). + */ + public static synchronized ServletContext getServletContext() { + + return PropertyMapLoader.getServletContext(); + } + + private static void initPropertyMap() { + + String fileName = System.getenv("EBEAN_PROPS_FILE"); + if (fileName == null) { + fileName = System.getProperty("ebean.props.file"); + if (fileName == null) { + fileName = "ebean.properties"; + } + } + + globalMap = PropertyMapLoader.load(null, fileName); + if (globalMap == null) { + // ebean.properties file was not found... but that + // is ok because we are likely doing programmatic config + globalMap = new PropertyMap(); + } + + String loaderCn = globalMap.get("ebean.properties.loader"); + if (loaderCn != null) { + // a Runnable that can be used to customise the initialisation + // of the GlobalProperties + try { + Runnable r = (Runnable) ClassUtil.newInstance(loaderCn); + r.run(); + } catch (Exception e) { + String m = "Error creating or running properties loader " + loaderCn; + throw new RuntimeException(m, e); + } + } + } + + /** + * Return the property map loading it if required. + */ + private static synchronized PropertyMap getPropertyMap() { + + if (globalMap == null) { + initPropertyMap(); + } + + return globalMap; + } + + /** + * Return a String property with a default value. + */ + public static synchronized String get(String key, String defaultValue) { + return getPropertyMap().get(key, defaultValue); + } + + /** + * Return a int property with a default value. + */ + public static synchronized int getInt(String key, int defaultValue) { + return getPropertyMap().getInt(key, defaultValue); + } + + /** + * Return a boolean property with a default value. + */ + public static synchronized boolean getBoolean(String key, boolean defaultValue) { + return getPropertyMap().getBoolean(key, defaultValue); + } + + /** + * Set a property return the previous value. This will evaluate any + * expressions in the value. + */ + public static synchronized String put(String key, String value) { + return getPropertyMap().putEval(key, value); + } + + /** + * Set a Map of key value properties. + */ + public static synchronized void putAll(Map keyValueMap) { + for (Entry e : keyValueMap.entrySet()) { + getPropertyMap().putEval(e.getKey(), e.getValue()); + } + } + + public static PropertySource getPropertySource(String name) { + return new ConfigPropertyMap(name); + } + + public static interface PropertySource { + + /** + * Return the name of the server. This is also the dataSource name. + */ + public String getServerName(); + + /** + * Get a property. This will prepend "ebean" and the server name to lookup + * the value. + */ + public String get(String key, String defaultValue); + + public int getInt(String key, int defaultValue); + + public boolean getBoolean(String key, boolean defaultValue); + + public > T getEnum(Class enumType, String key, T defaultValue); + + } + + public static class DelegatedGlobalPropertySource implements PropertySource { + + private String serverName; + + public DelegatedGlobalPropertySource(String serverName) { + this.serverName = serverName; + } + + @Override + public String getServerName() { + return serverName; + } + + @Override + public String get(String key, String defaultValue) { + return GlobalProperties.get(key, defaultValue); + } + + @Override + public int getInt(String key, int defaultValue) { + return GlobalProperties.getInt(key, defaultValue); + } + + @Override + public boolean getBoolean(String key, boolean defaultValue) { + return GlobalProperties.getBoolean(key, defaultValue); + } + + @Override + public > T getEnum(Class enumType, String key, T defaultValue) { + String level = get(key, defaultValue.name()); + return Enum.valueOf(enumType, level.toUpperCase()); + } + } +} diff --git a/src/main/java/com/avaje/ebean/config/MatchingNamingConvention.java b/src/main/java/com/avaje/ebean/config/MatchingNamingConvention.java new file mode 100644 index 000000000..a505f99ce --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/MatchingNamingConvention.java @@ -0,0 +1,46 @@ +package com.avaje.ebean.config; + +/** + * The JPA naming convention where column names match property names and table + * names match entity names. + * + *

    + * The JPA specification states that the in the case of no annotations the name + * of the class will be take as the table name and the name of a property will + * be taken as the name of the column. + *

    + * + * @author emcgreal + */ +public class MatchingNamingConvention extends AbstractNamingConvention { + + /** + * Create with a sequence format of "{table}_seq". + */ + public MatchingNamingConvention() { + super(); + } + + /** + * Instantiates with a specific format for DB sequences. + * + * @param sequenceFormat + * the sequence format + */ + public MatchingNamingConvention(String sequenceFormat) { + super(sequenceFormat); + } + + public String getColumnFromProperty(Class beanClass, String propertyName) { + return propertyName; + } + + public TableName getTableNameByConvention(Class beanClass) { + + return new TableName(getCatalog(), getSchema(), beanClass.getSimpleName()); + } + + public String getPropertyFromColumn(Class beanClass, String dbColumnName) { + return dbColumnName; + } +} diff --git a/src/main/java/com/avaje/ebean/config/NamingConvention.java b/src/main/java/com/avaje/ebean/config/NamingConvention.java new file mode 100644 index 000000000..af4f20653 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/NamingConvention.java @@ -0,0 +1,115 @@ +package com.avaje.ebean.config; + +import com.avaje.ebean.config.dbplatform.DatabasePlatform; + +/** + * Defines the naming convention for converting between logical property + * names/entity names and physical DB column names/table names. + *

    + * The main goal of the naming convention is to reduce the amount of + * configuration required in the mapping (especially when mapping between column + * and property names). + *

    + *

    + * Note that if you do not define a NamingConvention the default one will be + * used and you can configure it's behaviour via properties. + *

    + */ +public interface NamingConvention { + + /** + * Set the associated DatabasePlaform. + *

    + * This is set after the DatabasePlatform has been associated. + *

    + *

    + * The purpose of this is to enable NamingConvention to be able to support + * database platform specific configuration. + *

    + * + * @param databasePlatform + * the database platform + */ + public void setDatabasePlatform(DatabasePlatform databasePlatform); + + /** + * Returns the table name for a given Class. + *

    + * This method is always called and should take into account @Table + * annotations etc. This means you can choose to override the settings defined + * by @Table if you wish. + *

    + * + * @param beanClass + * the bean class + * + * @return the table name for the entity class + */ + public TableName getTableName(Class beanClass); + + /** + * Returns the ManyToMany join table name (aka the intersection table). + * + * @param lhsTable + * the left hand side bean table + * @param rhsTable + * the right hand side bean table + * + * @return the many to many join table name + */ + public TableName getM2MJoinTableName(TableName lhsTable, TableName rhsTable); + + /** + * Return the column name given the property name. + * + * @return the column name for a given property + */ + public String getColumnFromProperty(Class beanClass, String propertyName); + + /** + * Return the property name from the column name. + *

    + * This is used to help mapping of raw SQL queries onto bean properties. + *

    + * + * @param beanClass + * the bean class + * @param dbColumnName + * the db column name + * + * @return the property name from the column name + */ + public String getPropertyFromColumn(Class beanClass, String dbColumnName); + + /** + * Return the sequence name given the table name (for DB's that use + * sequences). + *

    + * Typically you might append "_seq" to the table name as an example. + *

    + * + * @param tableName + * the table name + * + * @return the sequence name + */ + public String getSequenceName(String tableName, String pkColumn); + + /** + * Return true if a prefix should be used building a foreign key name. + *

    + * This by default is true and this works well when the primary key column + * names are simply "ID". In this case a prefix (such as "ORDER" and + * "CUSTOMER" etc) is added to the foreign key column producing "ORDER_ID" and + * "CUSTOMER_ID". + *

    + *

    + * This should return false when your primary key columns are the same as the + * foreign key columns. For example, when the primary key columns are + * "ORDER_ID", "CUST_ID" etc ... and they are the same as the foreign key + * column names. + *

    + */ + public boolean isUseForeignKeyPrefix(); + +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/config/PropertyExpression.java b/src/main/java/com/avaje/ebean/config/PropertyExpression.java new file mode 100644 index 000000000..116d92461 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/PropertyExpression.java @@ -0,0 +1,185 @@ +package com.avaje.ebean.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.naming.InitialContext; +import javax.naming.NamingException; + +/** + * Helper used to evaluate expressions such as ${CATALINA_HOME}. + *

    + * The expressions can contain environment variables, system properties or JNDI + * properties. JNDI expressions take the form ${jndi:propertyName} where you + * substitute propertyName with the name of the jndi property you wish to + * evaluate. + *

    + */ +final class PropertyExpression { + + private static final Logger logger = LoggerFactory.getLogger(PropertyExpression.class); + + /** + * Prefix for looking up JNDI Environment variable. + */ + private static final String JAVA_COMP_ENV = "java:comp/env/"; + + /** + * Used to detect the start of an expression. + */ + private static String START = "${"; + + /** + * Used to detect the end of an expression. + */ + private static String END = "}"; + + /** + * Specify the PropertyHolder. + */ + private PropertyExpression() { + } + + /** + * Return the property value evaluating and replacing any expressions such as + * ${CATALINA_HOME}. + */ + static String eval(String val, PropertyMap map) { + if (val == null) { + return null; + } + int sp = val.indexOf(START); + if (sp > -1) { + int ep = val.indexOf(END, sp + 1); + if (ep > -1) { + return eval(val, sp, ep, map); + } + } + return val; + } + + /** + * Convert the expression using JNDI, Environment variables, System Properties + * or existing an property in SystemProperties itself. + */ + private static String evaluateExpression(String exp, PropertyMap map) { + + if (isJndiExpression(exp)) { + // JNDI property lookup... + String val = getJndiProperty(exp); + if (val != null) { + return val; + } + } + + // check Environment Variables first + String val = System.getenv(exp); + if (val == null) { + // then check system properties + val = System.getProperty(exp); + } + if (val == null && map != null) { + // then check PropertyMap + val = map.get(exp); + } + + if (val != null) { + return val; + + } else { + // unable to evaluate yet... but maybe later based on the order + // in which properties are being set/loaded. You can use + // GlobalProperties.evaluateExpressions() to get any unresolved + // expressions to be evaluated + logger.debug("Unable to evaluate expression [" + exp + "]"); + return null; + } + } + + private static String eval(String val, int sp, int ep, PropertyMap map) { + + StringBuilder sb = new StringBuilder(); + sb.append(val.substring(0, sp)); + + String cal = evalExpression(val, sp, ep, map); + sb.append(cal); + + eval(val, ep + 1, sb, map); + + return sb.toString(); + } + + private static void eval(String val, int startPos, StringBuilder sb, PropertyMap map) { + if (startPos < val.length()) { + int sp = val.indexOf(START, startPos); + if (sp > -1) { + // append what is between the last token and the new one (if startPos == + // sp nothing gets added) + sb.append(val.substring(startPos, sp)); + int ep = val.indexOf(END, sp + 1); + if (ep > -1) { + String cal = evalExpression(val, sp, ep, map); + sb.append(cal); + eval(val, ep + 1, sb, map); + return; + } + } + } + // append what is left... + sb.append(val.substring(startPos)); + } + + private static String evalExpression(String val, int sp, int ep, PropertyMap map) { + // trim off start and end ${ and } + String exp = val.substring(sp + START.length(), ep); + + // evaluate the variable + String evaled = evaluateExpression(exp, map); + if (evaled != null) { + return evaled; + } else { + // unable to evaluate at this stage (maybe later) + return START + exp + END; + } + } + + private static boolean isJndiExpression(String exp) { + if (exp.startsWith("JNDI:")) { + return true; + } + if (exp.startsWith("jndi:")) { + return true; + } + return false; + } + + /** + * Returns null if JNDI is not setup or if the property is not found. + * + * @param key + * the key of the JNDI Environment property including a JNDI: prefix. + */ + private static String getJndiProperty(String key) { + + try { + // remove the JNDI: prefix + key = key.substring(5); + + return (String) getJndiObject(key); + + } catch (NamingException ex) { + return null; + } + } + + /** + * Similar to getProperty but throws NamingException if JNDI is not setup or + * if the property is not found. + */ + private static Object getJndiObject(String key) throws NamingException { + + InitialContext ctx = new InitialContext(); + return ctx.lookup(JAVA_COMP_ENV + key); + } + +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/config/PropertyMap.java b/src/main/java/com/avaje/ebean/config/PropertyMap.java new file mode 100644 index 000000000..d431269f8 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/PropertyMap.java @@ -0,0 +1,95 @@ +package com.avaje.ebean.config; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +/** + * A map like structure of properties. + */ +final class PropertyMap implements Serializable { + + private static final long serialVersionUID = 1L; + + private LinkedHashMap map = new LinkedHashMap(); + + public String toString() { + return map.toString(); + } + + /** + * Go through all the properties and evaluate any expressions that have not + * been resolved. + */ + public void evaluateProperties() { + + for (Entry e : entrySet()) { + String key = e.getKey(); + String val = e.getValue(); + String eval = eval(val); + if (eval != null && !eval.equals(val)) { + put(key, eval); + } + } + } + + public synchronized String eval(String val) { + return PropertyExpression.eval(val, this); + } + + public synchronized boolean getBoolean(String key, boolean defaultValue) { + String value = get(key); + if (value == null) { + return defaultValue; + } else { + return Boolean.parseBoolean(value); + } + } + + public synchronized int getInt(String key, int defaultValue) { + String value = get(key); + if (value == null) { + return defaultValue; + } else { + return Integer.parseInt(value); + } + } + + public synchronized String get(String key, String defaultValue) { + String value = map.get(key.toLowerCase()); + return value == null ? defaultValue : value; + } + + public synchronized String get(String key) { + return map.get(key.toLowerCase()); + } + + synchronized void putAll(Map keyValueMap) { + Iterator> it = keyValueMap.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + put(entry.getKey(), entry.getValue()); + } + } + + synchronized String putEval(String key, String value) { + value = PropertyExpression.eval(value, this); + return map.put(key.toLowerCase(), value); + } + + synchronized String put(String key, String value) { + return map.put(key.toLowerCase(), value); + } + + synchronized String remove(String key) { + return map.remove(key.toLowerCase()); + } + + synchronized Set> entrySet() { + return map.entrySet(); + } + +} diff --git a/src/main/java/com/avaje/ebean/config/PropertyMapLoader.java b/src/main/java/com/avaje/ebean/config/PropertyMapLoader.java new file mode 100644 index 000000000..5adb466b8 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/PropertyMapLoader.java @@ -0,0 +1,161 @@ +package com.avaje.ebean.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; +import java.util.Map.Entry; + +import javax.servlet.ServletContext; + +/** + * Helper used to load the PropertyMap. + */ +final class PropertyMapLoader { + + private static final Logger logger = LoggerFactory.getLogger(PropertyMapLoader.class); + + private static ServletContext servletContext; + + /** + * Return the servlet context when in a web environment. + */ + public static ServletContext getServletContext() { + return servletContext; + } + + /** + * Set the ServletContext for when ebean.properties is in WEB-INF in a web + * application environment. + */ + public static void setServletContext(ServletContext servletContext) { + PropertyMapLoader.servletContext = servletContext; + } + + /** + * Load the file returning the property map. + * + * @param p + * an existing property map to load into. + * @param fileName + * the name of the properties file to load. + */ + public static PropertyMap load(PropertyMap p, String fileName) { + + InputStream is = findInputStream(fileName); + if (is == null) { + logger.error(fileName + " not found"); + return p; + } else { + return load(p, is); + } + } + + /** + * Load the inputstream returning the property map. + * + * @param p + * an existing property map to load into. + * @param in + * the InputStream of the properties file to load. + */ + private static PropertyMap load(PropertyMap p, InputStream in) { + + Properties props = new Properties(); + try { + props.load(in); + in.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (p == null) { + p = new PropertyMap(); + } + + // put values in initially without any evaluation + Iterator> it = props.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + String key = ((String) entry.getKey()).toLowerCase(); + String val = ((String) entry.getValue()); + if (val != null) { + val = val.trim(); + } + p.put(key, val); + } + + p.evaluateProperties(); + + String otherProps = p.remove("load.properties"); + if (otherProps == null) { + otherProps = p.remove("load.properties.override"); + } + if (otherProps != null) { + otherProps = otherProps.replace("\\", "/"); + InputStream is = findInputStream(otherProps); + if (is != null) { + logger.debug("loading properties from " + otherProps); + load(p, is); + } else { + logger.error("load.properties " + otherProps + " not found."); + } + } + + return p; + } + + /** + * Find the input stream given the file name. + */ + private static InputStream findInputStream(String fileName) { + + if (fileName == null) { + throw new NullPointerException("fileName is null?"); + } + + if (servletContext == null) { + logger.debug("No servletContext so not looking in WEB-INF for " + fileName); + + } else { + // first look in WEB-INF ... + InputStream in = servletContext.getResourceAsStream("/WEB-INF/" + fileName); + if (in != null) { + logger.debug(fileName + " found in WEB-INF"); + return in; + } + } + + try { + File f = new File(fileName); + + if (f.exists()) { + logger.debug(fileName + " found in file system"); + return new FileInputStream(f); + } else { + InputStream in = findInClassPath(fileName); + if (in != null) { + logger.debug(fileName + " found in classpath"); + } + return in; + } + + } catch (FileNotFoundException ex) { + // already made the check so this + // should never be thrown + throw new RuntimeException(ex); + } + } + + private static InputStream findInClassPath(String fileName) { + return Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName); + } + +} diff --git a/src/main/java/com/avaje/ebean/config/PstmtDelegate.java b/src/main/java/com/avaje/ebean/config/PstmtDelegate.java new file mode 100644 index 000000000..fedc47714 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/PstmtDelegate.java @@ -0,0 +1,24 @@ +package com.avaje.ebean.config; + +import java.sql.PreparedStatement; + +/** + * Unwrap the PreparedStatement to get the specific underlying implementation. + *

    + * This is used to handle specific JDBC driver issues. Typically this means + * getting the OraclePreparedStatement to handle Oracle specific issues etc. + *

    + * + * @author rbygrave + */ +public interface PstmtDelegate { + + /** + * Unwrap the PreparedStatement to get the specific underlying implementation. + * + * @param pstmt + * the PreparedStatement coming out of the connection pool + * @return the underlying PreparedStatement + */ + public PreparedStatement unwrap(PreparedStatement pstmt); +} diff --git a/src/main/java/com/avaje/ebean/config/ScalarTypeConverter.java b/src/main/java/com/avaje/ebean/config/ScalarTypeConverter.java new file mode 100644 index 000000000..1e70c56e1 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/ScalarTypeConverter.java @@ -0,0 +1,74 @@ +package com.avaje.ebean.config; + +/** + * Used to convert between a value object and a known scalar type. The value + * object is the logical type used in your application and the scalar type is + * the value used to persist than to the DB. + *

    + * The Value object should be immutable and scalar (aka not compound) and + * converts to and from a known scalar type which Ebean will use to persist the + * value. + *

    + *

    + * This is an easier alternative to implementing the + * com.avaje.ebean.server.type.ScalarType interface. + *

    + *

    + * Note that Ebean will automatically try to detect Immutable Scalar Value + * Objects and automatically support them via reflection. This however would not + * be appropriate when the logical type is different from the type you wish to + * use for persistence - for example, if the logical type was long and you + * wanted to use java.sql.Timestamp for persistence. In this case you would want + * to implement this interface rather than let Ebean automatically support that + * type via reflection. + *

    + *

    + * If you want to support a Compound Type rather than a Scalar Type refer to + * {@link CompoundType}. + *

    + * + * @author rbygrave + * + * @param + * The value object type. + * @param + * The scalar object type that is used to persist the value object. + * + * @see CompoundType + * @see CompoundTypeProperty + */ +public interface ScalarTypeConverter { + + /** + * Return the value to represent null. Typically this is actually null but for + * scala.Option and similar type converters this actually returns an instance + * representing "None". + */ + public B getNullValue(); + + /** + * Convert the scalar type value into the value object. + *

    + * This typically occurs when Ebean reads the value from a resultSet or other + * data source. + *

    + * + * @param scalarType + * the value from the data source + */ + public B wrapValue(S scalarType); + + /** + * Convert the value object into a scalar value that Ebean knows how to + * persist. + *

    + * This typically occurs when Ebean is persisting the value object to the data + * store. + *

    + * + * @param beanType + * the value object + */ + public S unwrapValue(B beanType); + +} diff --git a/src/main/java/com/avaje/ebean/config/ServerConfig.java b/src/main/java/com/avaje/ebean/config/ServerConfig.java new file mode 100644 index 000000000..cde17684a --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/ServerConfig.java @@ -0,0 +1,1541 @@ +package com.avaje.ebean.config; + +import java.util.ArrayList; +import java.util.List; + +import javax.sql.DataSource; + +import com.avaje.ebean.EbeanServer; +import com.avaje.ebean.EbeanServerFactory; +import com.avaje.ebean.LogLevel; +import com.avaje.ebean.Query; +import com.avaje.ebean.annotation.Encrypted; +import com.avaje.ebean.cache.ServerCacheFactory; +import com.avaje.ebean.cache.ServerCacheManager; +import com.avaje.ebean.config.GlobalProperties.PropertySource; +import com.avaje.ebean.config.dbplatform.DatabasePlatform; +import com.avaje.ebean.config.dbplatform.DbEncrypt; +import com.avaje.ebean.event.BeanPersistController; +import com.avaje.ebean.event.BeanPersistListener; +import com.avaje.ebean.event.BeanQueryAdapter; +import com.avaje.ebean.event.BulkTableEventListener; +import com.avaje.ebean.event.ServerConfigStartup; +import com.avaje.ebean.event.TransactionEventListener; +import com.avaje.ebean.util.ClassUtil; + +/** + * The configuration used for creating a EbeanServer. + *

    + * Used to programmatically construct an EbeanServer and optionally register it + * with the Ebean singleton. + *

    + *

    + * If you just use Ebean without this programmatic configuration Ebean will read + * the ebean.properties file and take the configuration from there. This usually + * includes searching the class path and automatically registering any entity + * classes and listeners etc. + *

    + * + *
    + * ServerConfig c = new ServerConfig();
    + * c.setName("ordh2");
    + * 
    + * // read the ebean.properties and load
    + * // those settings into this serverConfig object
    + * c.loadFromProperties();
    + * 
    + * // generate DDL and run it
    + * c.setDdlGenerate(true);
    + * c.setDdlRun(true);
    + * 
    + * // add any classes found in the app.data package
    + * c.addPackage("app.data");
    + * 
    + * // add the names of Jars that contain entities
    + * c.addJar("myJarContainingEntities.jar");
    + * c.addJar("someOtherJarContainingEntities.jar");
    + * 
    + * // register as the 'Default' server
    + * c.setDefaultServer(true);
    + * 
    + * EbeanServer server = EbeanServerFactory.create(c);
    + * 
    + * 
    + * + * @see EbeanServerFactory + * + * @author emcgreal + * @author rbygrave + */ +public class ServerConfig { + /** The Constant DEFAULT_QUERY_BATCH_SIZE. Default: 100 */ + private final static int DEFAULT_QUERY_BATCH_SIZE = 100; + + /** + * The EbeanServer name. + */ + private String name; + + /** + * The resource directory. + */ + private String resourceDirectory; + + /** + * The enhance log level. Used with subclass generation. + */ + private int enhanceLogLevel; + + /** + * Set to true to register this EbeanServer with the Ebean singleton. + */ + private boolean register = true; + + /** + * Set to true if this is the default/primary server. + */ + private boolean defaultServer; + + /** + * List of interesting classes such as entities, embedded, ScalarTypes, + * Listeners, Finders, Controllers etc. + */ + private List> classes = new ArrayList>(); + + /** + * The packages that are searched for interesting classes. Only used when + * classes is empty/not explicitly specified. + */ + private List packages = new ArrayList(); + + /** + * The names of Jar files that are searched for entities and other interesting + * classes. Only used when classes is empty/not explicitly specified. + */ + private List searchJars = new ArrayList(); + + /** The autofetch config. */ + private AutofetchConfig autofetchConfig = new AutofetchConfig(); + + /** The database platform name. */ + private String databasePlatformName; + + /** The database platform. */ + private DatabasePlatform databasePlatform; + + /** + * For DB's using sequences this is the number of sequence values prefetched. + */ + private int databaseSequenceBatchSize = 20; + + private boolean persistBatching; + + private int persistBatchSize = 20; + + /** The default batch size for lazy loading */ + private int lazyLoadBatchSize = 1; + + /** The query batch size. */ + private int queryBatchSize = -1; + + private boolean ddlGenerate; + + private boolean ddlRun; + + private boolean debugSql; + + private boolean debugLazyLoad; + + private boolean useJtaTransactionManager; + + /** + * The external transaction manager (like Spring). + */ + private ExternalTransactionManager externalTransactionManager; + + /** + * Set to true to log using java.util.logging and otherwise uses ebean + * transaction loggers. + */ + private boolean loggingToJavaLogger; + + /** + * The directory transaction logs go (when loggingToJavaLogger is false). + */ + private String loggingDirectory = "logs"; + + /** + * The overall transaction logging level. + */ + private LogLevel loggingLevel = LogLevel.NONE; + + /** + * Used to unwrap PreparedStatements to perform JDBC Driver specific functions + */ + private PstmtDelegate pstmtDelegate; + + /** The data source. */ + private DataSource dataSource; + + /** The data source config. */ + private DataSourceConfig dataSourceConfig = new DataSourceConfig(); + + /** The data source jndi name. */ + private String dataSourceJndiName; + + /** The database boolean true. */ + private String databaseBooleanTrue; + + /** The database boolean false. */ + private String databaseBooleanFalse; + + /** The naming convention. */ + private NamingConvention namingConvention; + + /** The update changes only. */ + private boolean updateChangesOnly = true; + + private List persistControllers = new ArrayList(); + private List> persistListeners = new ArrayList>(); + private List queryAdapters = new ArrayList(); + private List bulkTableEventListeners = new ArrayList(); + private List configStartupListeners = new ArrayList(); + private List transactionEventListeners = new ArrayList(); + + private EncryptKeyManager encryptKeyManager; + + private EncryptDeployManager encryptDeployManager; + + private Encryptor encryptor; + + private DbEncrypt dbEncrypt; + + private ServerCacheFactory serverCacheFactory; + + private ServerCacheManager serverCacheManager; + + /** + * Set this to true when by default vanilla objects should be returned from + * queries rather than dynamic subclasses etc. Only relevant when not using + * enhancement (using dynamic subclasses). + */ + private boolean vanillaMode; + + /** + * Controls whether the {@link EbeanServer#getReference(Class, Object)} method + * returns vanilla objects or not. + */ + private boolean vanillaRefMode; + + /** + * Set to false to require enhancement to be used. Defaults to true. + */ + private boolean allowSubclassing = true; + + /** + * Construct a Server Configuration for programmatically creating an + * EbeanServer. + */ + public ServerConfig() { + + } + + /** + * Return the name of the EbeanServer. + */ + public String getName() { + return name; + } + + /** + * Set the name of the EbeanServer. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Return true if this server should be registered with the Ebean singleton + * when it is created. + *

    + * By default this is set to true. + *

    + */ + public boolean isRegister() { + return register; + } + + /** + * Set to false if you do not want this server to be registered with the Ebean + * singleton when it is created. + *

    + * By default this is set to true. + *

    + */ + public void setRegister(boolean register) { + this.register = register; + } + + /** + * Return true if this server should be registered as the "default" server + * with the Ebean singleton. + *

    + * This is only used when {@link #setRegister(boolean)} is also true. + *

    + */ + public boolean isDefaultServer() { + return defaultServer; + } + + /** + * Set true if this EbeanServer should be registered as the "default" server + * with the Ebean singleton. + *

    + * This is only used when {@link #setRegister(boolean)} is also true. + *

    + */ + public void setDefaultServer(boolean defaultServer) { + this.defaultServer = defaultServer; + } + + /** + * Returns true if by default JDBC batching is used for persisting or deleting + * beans. + *

    + * With this Ebean will batch up persist requests and use the JDBC batch api. + * This is a performance optimisation designed to reduce the network chatter. + *

    + */ + public boolean isPersistBatching() { + return persistBatching; + } + + /** + * Use isPersistBatching() instead. + * + * @deprecated + */ + public boolean isUsePersistBatching() { + return persistBatching; + } + + /** + * Set to true if you what to use JDBC batching for persisting and deleting + * beans. + *

    + * With this Ebean will batch up persist requests and use the JDBC batch api. + * This is a performance optimisation designed to reduce the network chatter. + *

    + */ + public void setPersistBatching(boolean persistBatching) { + this.persistBatching = persistBatching; + } + + /** + * Use setPersistBatching() instead. + * + * @deprecated + */ + public void setUsePersistBatching(boolean persistBatching) { + this.persistBatching = persistBatching; + } + + /** + * Return the batch size used for JDBC batching. This defaults to 20. + */ + public int getPersistBatchSize() { + return persistBatchSize; + } + + /** + * Set the batch size used for JDBC batching. If unset this defaults to 20. + */ + public void setPersistBatchSize(int persistBatchSize) { + this.persistBatchSize = persistBatchSize; + } + + /** + * Return the default batch size for lazy loading of beans and collections. + */ + public int getLazyLoadBatchSize() { + return lazyLoadBatchSize; + } + + /** + * Gets the query batch size. + * + * @return the query batch size + */ + public int getQueryBatchSize() { + return queryBatchSize; + } + + /** + * Sets the query batch size. + * + * @param queryBatchSize + * the new query batch size + */ + public void setQueryBatchSize(int queryBatchSize) { + this.queryBatchSize = queryBatchSize; + } + + /** + * Set the default batch size for lazy loading. + *

    + * This is the number of beans or collections loaded when lazy loading is + * invoked by default. + *

    + *

    + * The default value is for this is 1 (load 1 bean or collection). + *

    + *

    + * You can explicitly control the lazy loading batch size for a given join on + * a query using +lazy(batchSize) or JoinConfig. + *

    + */ + public void setLazyLoadBatchSize(int lazyLoadBatchSize) { + this.lazyLoadBatchSize = lazyLoadBatchSize; + } + + /** + * Set the number of sequences to fetch/preallocate when using DB sequences. + *

    + * This is a performance optimisation to reduce the number times Ebean + * requests a sequence to be used as an Id for a bean (aka reduce network + * chatter). + *

    + */ + public void setDatabaseSequenceBatchSize(int databaseSequenceBatchSize) { + this.databaseSequenceBatchSize = databaseSequenceBatchSize; + } + + /** + * Return true if we are running in a JTA Transaction manager. + */ + public boolean isUseJtaTransactionManager() { + return useJtaTransactionManager; + } + + /** + * Set to true if we are running in a JTA Transaction manager. + */ + public void setUseJtaTransactionManager(boolean useJtaTransactionManager) { + this.useJtaTransactionManager = useJtaTransactionManager; + } + + /** + * Return the external transaction manager. + */ + public ExternalTransactionManager getExternalTransactionManager() { + return externalTransactionManager; + } + + /** + * Set the external transaction manager. + */ + public void setExternalTransactionManager(ExternalTransactionManager externalTransactionManager) { + this.externalTransactionManager = externalTransactionManager; + } + + /** + * Return the ServerCacheFactory. + */ + public ServerCacheFactory getServerCacheFactory() { + return serverCacheFactory; + } + + /** + * Set the ServerCacheFactory to use. + */ + public void setServerCacheFactory(ServerCacheFactory serverCacheFactory) { + this.serverCacheFactory = serverCacheFactory; + } + + /** + * Return the ServerCacheManager. + */ + public ServerCacheManager getServerCacheManager() { + return serverCacheManager; + } + + /** + * Set the ServerCacheManager to use. + */ + public void setServerCacheManager(ServerCacheManager serverCacheManager) { + this.serverCacheManager = serverCacheManager; + } + + /** + * Return true if by default queries should return 'vanilla' objects rather + * than dynamic subclasses. + *

    + * This setting is not relevant when using enhancement (only when using + * dynamic subclasses). + *

    + */ + public boolean isVanillaMode() { + return vanillaMode; + } + + /** + * Set this to true if by default queries should return 'vanilla' objects + * rather than dynamic subclasses. + *

    + * This setting is not relevant when using enhancement (only when using + * dynamic subclasses). + *

    + *

    + * Alternatively you can set this on a specific query via + * {@link Query#setVanillaMode(boolean)}. + *

    + * + * @see #setVanillaRefMode(boolean) + * @see Query#setVanillaMode(boolean) + */ + public void setVanillaMode(boolean vanillaMode) { + this.vanillaMode = vanillaMode; + } + + /** + * Returns true if {@link EbeanServer#getReference(Class, Object)} should + * return vanilla objects or not. + * + * @see #setVanillaMode(boolean) + * @see Query#setVanillaMode(boolean) + */ + public boolean isVanillaRefMode() { + return vanillaRefMode; + } + + /** + * Set this to true if you want + * {@link EbeanServer#getReference(Class, Object)} to return vanilla objects. + */ + public void setVanillaRefMode(boolean vanillaRefMode) { + this.vanillaRefMode = vanillaRefMode; + } + + /** + * Return the log level used for "subclassing" enhancement. + */ + public int getEnhanceLogLevel() { + return enhanceLogLevel; + } + + /** + * Set the log level used for "subclassing" enhancement. + */ + public void setEnhanceLogLevel(int enhanceLogLevel) { + this.enhanceLogLevel = enhanceLogLevel; + } + + /** + * Return the NamingConvention. + *

    + * If none has been set the default UnderscoreNamingConvention is used. + *

    + */ + public NamingConvention getNamingConvention() { + return namingConvention; + } + + /** + * Set the NamingConvention. + *

    + * If none is set the default UnderscoreNamingConvention is used. + *

    + */ + public void setNamingConvention(NamingConvention namingConvention) { + this.namingConvention = namingConvention; + } + + /** + * Return the configuration for the Autofetch feature. + */ + public AutofetchConfig getAutofetchConfig() { + return autofetchConfig; + } + + /** + * Set the configuration for the Autofetch feature. + */ + public void setAutofetchConfig(AutofetchConfig autofetchConfig) { + this.autofetchConfig = autofetchConfig; + } + + /** + * Return the PreparedStatementDelegate. + */ + public PstmtDelegate getPstmtDelegate() { + return pstmtDelegate; + } + + /** + * Set the PstmtDelegate which can be used to support JDBC driver specific + * features. + *

    + * Typically this means Oracle JDBC driver specific workarounds. + *

    + */ + public void setPstmtDelegate(PstmtDelegate pstmtDelegate) { + this.pstmtDelegate = pstmtDelegate; + } + + /** + * Return the DataSource. + */ + public DataSource getDataSource() { + return dataSource; + } + + /** + * Set a DataSource. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + /** + * Return the configuration to build a DataSource using Ebean's own DataSource + * implementation. + */ + public DataSourceConfig getDataSourceConfig() { + return dataSourceConfig; + } + + /** + * Set the configuration required to build a DataSource using Ebean's own + * DataSource implementation. + */ + public void setDataSourceConfig(DataSourceConfig dataSourceConfig) { + this.dataSourceConfig = dataSourceConfig; + } + + /** + * Return the JNDI name of the DataSource to use. + */ + public String getDataSourceJndiName() { + return dataSourceJndiName; + } + + /** + * Set the JNDI name of the DataSource to use. + *

    + * By default a prefix of "java:comp/env/jdbc/" is used to lookup the + * DataSource. This prefix is not used if dataSourceJndiName starts with + * "java:". + *

    + */ + public void setDataSourceJndiName(String dataSourceJndiName) { + this.dataSourceJndiName = dataSourceJndiName; + } + + /** + * Return a value used to represent TRUE in the database. + *

    + * This is used for databases that do not support boolean natively. + *

    + *

    + * The value returned is either a Integer or a String (e.g. "1", or "T"). + *

    + */ + public String getDatabaseBooleanTrue() { + return databaseBooleanTrue; + } + + /** + * Set the value to represent TRUE in the database. + *

    + * This is used for databases that do not support boolean natively. + *

    + *

    + * The value set is either a Integer or a String (e.g. "1", or "T"). + *

    + */ + public void setDatabaseBooleanTrue(String databaseTrue) { + this.databaseBooleanTrue = databaseTrue; + } + + /** + * Return a value used to represent FALSE in the database. + *

    + * This is used for databases that do not support boolean natively. + *

    + *

    + * The value returned is either a Integer or a String (e.g. "0", or "F"). + *

    + */ + public String getDatabaseBooleanFalse() { + return databaseBooleanFalse; + } + + /** + * Set the value to represent FALSE in the database. + *

    + * This is used for databases that do not support boolean natively. + *

    + *

    + * The value set is either a Integer or a String (e.g. "0", or "F"). + *

    + */ + public void setDatabaseBooleanFalse(String databaseFalse) { + this.databaseBooleanFalse = databaseFalse; + } + + /** + * Return the number of DB sequence values that should be preallocated. + */ + public int getDatabaseSequenceBatchSize() { + return databaseSequenceBatchSize; + } + + /** + * Set the number of DB sequence values that should be preallocated and cached + * by Ebean. + *

    + * This is only used for DB's that use sequences and is a performance + * optimisation. This reduces the number of times Ebean needs to get a + * sequence value from the Database reducing network chatter. + *

    + *

    + * By default this value is 10 so when we need another Id (and don't have one + * in our cache) Ebean will fetch 10 id's from the database. Note that when + * the cache drops to have full (which is 5 by default) Ebean will fetch + * another batch of Id's in a background thread. + *

    + */ + public void setDatabaseSequenceBatch(int databaseSequenceBatchSize) { + this.databaseSequenceBatchSize = databaseSequenceBatchSize; + } + + /** + * Return the database platform name (can be null). + *

    + * If null then the platform is determined automatically via the JDBC driver + * information. + *

    + */ + public String getDatabasePlatformName() { + return databasePlatformName; + } + + /** + * Explicitly set the database platform name + *

    + * If none is set then the platform is determined automatically via the JDBC + * driver information. + *

    + *

    + * This can be used when the Database Platform can not be automatically + * detected from the JDBC driver (possibly 3rd party JDBC driver). It is also + * useful when you want to do offline DDL generation for a database platform + * that you don't have access to. + *

    + *

    + * Values are oracle, h2, postgres, mysql, mssqlserver2005. + *

    + * + * @see DataSourceConfig#setOffline(boolean) + */ + public void setDatabasePlatformName(String databasePlatformName) { + this.databasePlatformName = databasePlatformName; + } + + /** + * Return the database platform to use for this server. + */ + public DatabasePlatform getDatabasePlatform() { + return databasePlatform; + } + + /** + * Explicitly set the database platform to use. + *

    + * If none is set then the platform is determined via the databasePlatformName + * or automatically via the JDBC driver information. + *

    + */ + public void setDatabasePlatform(DatabasePlatform databasePlatform) { + this.databasePlatform = databasePlatform; + } + + /** + * Return the EncryptKeyManager. + */ + public EncryptKeyManager getEncryptKeyManager() { + return encryptKeyManager; + } + + /** + * Set the EncryptKeyManager. + *

    + * This is required when you want to use encrypted properties. + *

    + *

    + * You can also set this in ebean.proprerties: + *

    + * + *
    +   * # set via ebean.properties
    +   * 
    +   * ebean.encryptKeyManager=com.avaje.tests.basic.encrypt.BasicEncyptKeyManager
    +   * 
    + */ + public void setEncryptKeyManager(EncryptKeyManager encryptKeyManager) { + this.encryptKeyManager = encryptKeyManager; + } + + /** + * Return the EncryptDeployManager. + *

    + * This is optionally used to programmatically define which columns are + * encrypted instead of using the {@link Encrypted} Annotation. + *

    + */ + public EncryptDeployManager getEncryptDeployManager() { + return encryptDeployManager; + } + + /** + * Set the EncryptDeployManager. + *

    + * This is optionally used to programmatically define which columns are + * encrypted instead of using the {@link Encrypted} Annotation. + *

    + */ + public void setEncryptDeployManager(EncryptDeployManager encryptDeployManager) { + this.encryptDeployManager = encryptDeployManager; + } + + /** + * Return the Encryptor used to encrypt data on the java client side (as + * opposed to DB encryption functions). + */ + public Encryptor getEncryptor() { + return encryptor; + } + + /** + * Set the Encryptor used to encrypt data on the java client side (as opposed + * to DB encryption functions). + *

    + * Ebean has a default implementation that it will use if you do not set your + * own Encryptor implementation. + *

    + */ + public void setEncryptor(Encryptor encryptor) { + this.encryptor = encryptor; + } + + /** + * Return the DbEncrypt used to encrypt and decrypt properties. + *

    + * Note that if this is not set then the DbPlatform may already have a + * DbEncrypt set and that will be used. + *

    + */ + public DbEncrypt getDbEncrypt() { + return dbEncrypt; + } + + /** + * Set the DbEncrypt used to encrypt and decrypt properties. + *

    + * Note that if this is not set then the DbPlatform may already have a + * DbEncrypt set (H2, MySql, Postgres and Oracle platforms have a DbEncrypt) + *

    + */ + public void setDbEncrypt(DbEncrypt dbEncrypt) { + this.dbEncrypt = dbEncrypt; + } + + /** + * Return true to get the generated SQL queries output to the console. + *

    + * To get the SQL and bind variables for insert update delete statements you + * should use transaction logging. + *

    + */ + public boolean isDebugSql() { + return debugSql; + } + + /** + * Set to true to get the generated SQL queries output to the console. + *

    + * To get the SQL and bind variables for insert update delete statements you + * should use transaction logging. + *

    + */ + public void setDebugSql(boolean debugSql) { + this.debugSql = debugSql; + } + + /** + * Return true if there is debug logging on lazy loading events. + */ + public boolean isDebugLazyLoad() { + return debugLazyLoad; + } + + /** + * Set to true to get debug logging on lazy loading events. + */ + public void setDebugLazyLoad(boolean debugLazyLoad) { + this.debugLazyLoad = debugLazyLoad; + } + + /** + * Return the default transaction logging level. + *

    + * The logging level can be changed on a per transaction basis. + *

    + */ + public LogLevel getLoggingLevel() { + return loggingLevel; + } + + /** + * Set the default transaction logging level. + *

    + * The logging level can be changed on a per transaction basis. + *

    + */ + public void setLoggingLevel(LogLevel logLevel) { + this.loggingLevel = logLevel; + } + + /** + * Return the directory where transaction logs go. + */ + public String getLoggingDirectory() { + return loggingDirectory; + } + + /** + * Return the transaction log directory substituting any expressions such as + * ${catalina.base} etc. + */ + public String getLoggingDirectoryWithEval() { + return GlobalProperties.evaluateExpressions(loggingDirectory); + } + + /** + * Set the directory that the transaction logs go in. + *

    + * This will not be used if the transaction logging is going to java util + * logging (via {@link #setLoggingToJavaLogger(boolean)}). + *

    + *

    + * This can contain expressions like ${catalina.base} with environment + * variables, java system properties and entries in ebean.properties. + *

    + *

    + * e.g. ${catalina.base}/logs/trans + *

    + * + * @param loggingDirectory + * the transaction log directory + */ + public void setLoggingDirectory(String loggingDirectory) { + this.loggingDirectory = loggingDirectory; + } + + /** + * Return true if you want to use a java.util.logging.Logger to log + * transaction statements, bind values etc. + *

    + * If this is false then the default transaction logger is used which logs the + * transaction details to separate transaction log files. + *

    + */ + @Deprecated + public boolean isLoggingToJavaLogger() { + return loggingToJavaLogger; + } + + /** + * Set this to true if you want transaction logging to use a + * java.util.logging.Logger to log the statements and bind variables etc + * rather than the default one which creates separate transaction log files. + */ + @Deprecated + public void setLoggingToJavaLogger(boolean transactionLogToJavaLogger) { + this.loggingToJavaLogger = transactionLogToJavaLogger; + } + + /** + * Deprecated - please use isTransactionLogToJavaLogger(); + * + * @deprecated + */ + public boolean isUseJuliTransactionLogger() { + return isLoggingToJavaLogger(); + } + + /** + * Deprecated - please use setTransactionLogToJavaLogger(); + * + * @deprecated + */ + public void setUseJuliTransactionLogger(boolean transactionLogToJavaLogger) { + setLoggingToJavaLogger(transactionLogToJavaLogger); + } + + /** + * Set to true to run the DDL generation on startup. + */ + public void setDdlGenerate(boolean ddlGenerate) { + this.ddlGenerate = ddlGenerate; + } + + /** + * Set to true to run the generated DDL on startup. + */ + public void setDdlRun(boolean ddlRun) { + this.ddlRun = ddlRun; + } + + /** + * Return true if the DDL should be generated. + */ + public boolean isDdlGenerate() { + return ddlGenerate; + } + + /** + * Return true if the DDL should be run. + */ + public boolean isDdlRun() { + return ddlRun; + } + + /** + * Programmatically add classes (typically entities) that this server should + * use. + *

    + * The class can be an Entity, Embedded type, ScalarType, BeanPersistListener, + * BeanFinder or BeanPersistController. + *

    + *

    + * If no classes are specified then the classes are found automatically via + * searching the class path. + *

    + *

    + * Alternatively the classes can be added via {@link #setClasses(List)}. + *

    + * + * @param cls + * the entity type (or other type) that should be registered by this + * server. + */ + public void addClass(Class cls) { + if (classes == null) { + classes = new ArrayList>(); + } + classes.add(cls); + } + + /** + * Add a package to search for entities via class path search. + *

    + * This is only used if classes have not been explicitly specified. + *

    + */ + public void addPackage(String packageName) { + if (packages == null) { + packages = new ArrayList(); + } + packages.add(packageName); + } + + /** + * Return packages to search for entities via class path search. + *

    + * This is only used if classes have not been explicitly specified. + *

    + */ + public List getPackages() { + return packages; + } + + /** + * Set packages to search for entities via class path search. + *

    + * This is only used if classes have not been explicitly specified. + *

    + */ + public void setPackages(List packages) { + this.packages = packages; + } + + /** + * Add the name of a Jar to search for entities via class path search. + *

    + * This is only used if classes have not been explicitly specified. + *

    + *

    + * If you are using ebean.properties you can specify jars to search by setting + * a ebean.search.jars property. + *

    + * + *
    +   * # EBean will search through classes for entities, but will not search jar files 
    +   * # unless you tell it to do so, for performance reasons.  Set this value to a 
    +   * # comma-delimited list of jar files you want ebean to search.
    +   * ebean.search.jars=example.jar
    +   * 
    + */ + public void addJar(String jarName) { + if (searchJars == null) { + searchJars = new ArrayList(); + } + searchJars.add(jarName); + } + + /** + * Return packages to search for entities via class path search. + *

    + * This is only used if classes have not been explicitly specified. + *

    + */ + public List getJars() { + return searchJars; + } + + /** + * Set the names of Jars to search for entities via class path search. + *

    + * This is only used if classes have not been explicitly specified. + *

    + */ + public void setJars(List searchJars) { + this.searchJars = searchJars; + } + + /** + * Set the list of classes (entities, listeners, scalarTypes etc) that should + * be used for this server. + *

    + * If no classes are specified then the classes are found automatically via + * searching the class path. + *

    + *

    + * Alternatively the classes can contain added via {@link #addClass(Class)}. + *

    + */ + public void setClasses(List> classes) { + this.classes = classes; + } + + /** + * Return the classes registered for this server. Typically this includes + * entities and perhaps listeners. + */ + public List> getClasses() { + return classes; + } + + /** + * Return true to only update changed properties. + */ + public boolean isUpdateChangesOnly() { + return updateChangesOnly; + } + + /** + * Set to true to only update changed properties. + */ + public void setUpdateChangesOnly(boolean updateChangesOnly) { + this.updateChangesOnly = updateChangesOnly; + } + + /** + * Set to false to require enhancement to be used. Defaults to true. + */ + public void setAllowSubclassing(boolean allowSubclassing) { + this.allowSubclassing = allowSubclassing; + } + + /** + * Returns whether this config supports subclassed entities. + */ + public boolean isAllowSubclassing() { + return allowSubclassing; + } + + /** + * Returns the resource directory. + */ + public String getResourceDirectory() { + return resourceDirectory; + } + + /** + * Sets the resource directory. + */ + public void setResourceDirectory(String resourceDirectory) { + this.resourceDirectory = resourceDirectory; + } + + /** + * Register a BeanQueryAdapter instance. + *

    + * Note alternatively you can use {@link #setQueryAdapters(List)} to set all + * the BeanQueryAdapter instances. + *

    + */ + public void add(BeanQueryAdapter beanQueryAdapter) { + queryAdapters.add(beanQueryAdapter); + } + + /** + * Return the BeanQueryAdapter instances. + */ + public List getQueryAdapters() { + return queryAdapters; + } + + /** + * Register all the BeanQueryAdapter instances. + *

    + * Note alternatively you can use {@link #add(BeanQueryAdapter)} to add + * BeanQueryAdapter instances one at a time. + *

    + */ + public void setQueryAdapters(List queryAdapters) { + this.queryAdapters = queryAdapters; + } + + /** + * Register a BeanPersistController instance. + *

    + * Note alternatively you can use {@link #setPersistControllers(List)} to set + * all the BeanPersistController instances. + *

    + */ + public void add(BeanPersistController beanPersistController) { + persistControllers.add(beanPersistController); + } + + /** + * Return the BeanPersistController instances. + */ + public List getPersistControllers() { + return persistControllers; + } + + /** + * Register all the BeanPersistController instances. + *

    + * Note alternatively you can use {@link #add(BeanPersistController)} to add + * BeanPersistController instances one at a time. + *

    + */ + public void setPersistControllers(List persistControllers) { + this.persistControllers = persistControllers; + } + + /** + * Register a TransactionEventListener instance + *

    + * Note alternatively you can use {@link #setTransactionEventListeners(List)} + * to set all the TransactionEventListener instances. + *

    + */ + public void add(TransactionEventListener listener) { + transactionEventListeners.add(listener); + } + + /** + * Return the TransactionEventListener instances. + */ + public List getTransactionEventListeners() { + return transactionEventListeners; + } + + /** + * Register all the TransactionEventListener instances. + *

    + * Note alternatively you can use {@link #add(TransactionEventListener)} to + * add TransactionEventListener instances one at a time. + *

    + */ + public void setTransactionEventListeners(List transactionEventListeners) { + this.transactionEventListeners = transactionEventListeners; + } + + /** + * Register a BeanPersistListener instance. + *

    + * Note alternatively you can use {@link #setPersistListeners(List)} to set + * all the BeanPersistListener instances. + *

    + */ + public void add(BeanPersistListener beanPersistListener) { + persistListeners.add(beanPersistListener); + } + + /** + * Return the BeanPersistListener instances. + */ + public List> getPersistListeners() { + return persistListeners; + } + + /** + * Add a BulkTableEventListener + */ + public void add(BulkTableEventListener bulkTableEventListener) { + bulkTableEventListeners.add(bulkTableEventListener); + } + + /** + * Return the list of BulkTableEventListener instances. + */ + public List getBulkTableEventListeners() { + return bulkTableEventListeners; + } + + /** + * Add a ServerConfigStartup. + */ + public void addServerConfigStartup(ServerConfigStartup configStartupListener) { + configStartupListeners.add(configStartupListener); + } + + /** + * Return the list of ServerConfigStartup instances. + */ + public List getServerConfigStartupListeners() { + return configStartupListeners; + } + + /** + * Register all the BeanPersistListener instances. + *

    + * Note alternatively you can use {@link #add(BeanPersistListener)} to add + * BeanPersistListener instances one at a time. + *

    + */ + public void setPersistListeners(List> persistListeners) { + this.persistListeners = persistListeners; + } + + /** + * Load the settings from the ebean.properties file. + */ + public void loadFromProperties() { + ConfigPropertyMap p = new ConfigPropertyMap(name); + loadSettings(p); + } + + /** + * Return a PropertySource for this server. + */ + public PropertySource getPropertySource() { + return GlobalProperties.getPropertySource(name); + } + + /** + * Return a configuration property using a default value. + */ + public String getProperty(String propertyName, String defaultValue) { + PropertySource p = new ConfigPropertyMap(name); + return p.get(propertyName, defaultValue); + } + + /** + * Return a configuration property. + */ + public String getProperty(String propertyName) { + return getProperty(propertyName, null); + } + + @SuppressWarnings("unchecked") + private T createInstance(PropertySource p, Class type, String key) { + + String classname = p.get(key, null); + if (classname == null) { + return null; + } + + return (T) ClassUtil.newInstance(classname); + } + + /** + * loads the data source settings to preserve existing behaviour. IMHO, if someone has set the datasource config already, + * they don't want the settings to be reloaded and reset. This allows a descending class to override this behaviour and prevent it + * from happening. + * + * @param p - The defined property source passed to load settings + */ + protected void loadDataSourceSettings(PropertySource p) { + dataSourceConfig.loadSettings(p.getServerName()); + } + + /** + * This is broken out for the same reason as above - preserve existing behaviour but let it be overridden. + * + * @param p + */ + protected void loadAutofetchConfig(PropertySource p) { + autofetchConfig.loadSettings(p); + } + + /** + * Load the configuration settings from the properties file. + */ + protected void loadSettings(PropertySource p) { + + if (autofetchConfig == null) { + autofetchConfig = new AutofetchConfig(); + } + + loadAutofetchConfig(p); + + if (dataSourceConfig == null) { + dataSourceConfig = new DataSourceConfig(); + } + + loadDataSourceSettings(p); + + useJtaTransactionManager = p.getBoolean("useJtaTransactionManager", false); + namingConvention = createNamingConvention(p); + databasePlatform = createInstance(p, DatabasePlatform.class, "databasePlatform"); + encryptKeyManager = createInstance(p, EncryptKeyManager.class, "encryptKeyManager"); + encryptDeployManager = createInstance(p, EncryptDeployManager.class, "encryptDeployManager"); + encryptor = createInstance(p, Encryptor.class, "encryptor"); + dbEncrypt = createInstance(p, DbEncrypt.class, "dbEncrypt"); + serverCacheFactory = createInstance(p, ServerCacheFactory.class, "serverCacheFactory"); + serverCacheManager = createInstance(p, ServerCacheManager.class, "serverCacheManager"); + + String jarsProp = p.get("search.jars", p.get("jars", null)); + if (jarsProp != null) { + searchJars = getSearchJarsPackages(jarsProp); + } + + String packagesProp = p.get("search.packages", p.get("packages", null)); + if (packages != null) { + packages = getSearchJarsPackages(packagesProp); + } + + allowSubclassing = p.getBoolean("allowSubclassing", true); + vanillaMode = p.getBoolean("vanillaMode", false); + vanillaRefMode = p.getBoolean("vanillaRefMode", false); + updateChangesOnly = p.getBoolean("updateChangesOnly", true); + + boolean batchMode = p.getBoolean("batch.mode", false); + persistBatching = p.getBoolean("persistBatching", batchMode); + + int batchSize = p.getInt("batch.size", 20); + persistBatchSize = p.getInt("persistBatchSize", batchSize); + + dataSourceJndiName = p.get("dataSourceJndiName", null); + databaseSequenceBatchSize = p.getInt("databaseSequenceBatchSize", 20); + databaseBooleanTrue = p.get("databaseBooleanTrue", null); + databaseBooleanFalse = p.get("databaseBooleanFalse", null); + databasePlatformName = p.get("databasePlatformName", null); + + lazyLoadBatchSize = p.getInt("lazyLoadBatchSize", 1); + queryBatchSize = p.getInt("queryBatchSize", DEFAULT_QUERY_BATCH_SIZE); + + ddlGenerate = p.getBoolean("ddl.generate", false); + ddlRun = p.getBoolean("ddl.run", false); + debugSql = p.getBoolean("debug.sql", false); + debugLazyLoad = p.getBoolean("debug.lazyload", false); + + loggingLevel = getLogLevelValue(p); + + String s = p.get("useJuliTransactionLogger", null); + s = p.get("loggingToJavaLogger", s); + loggingToJavaLogger = "true".equalsIgnoreCase(s); + + s = p.get("log.directory", "logs"); + loggingDirectory = p.get("logging.directory", s); + + classes = getClasses(p); + } + + private LogLevel getLogLevelValue(PropertySource p) { + // logging.level preferred but others parameters will work + String logValue = p.get("logging", "NONE"); + logValue = p.get("log.level", logValue); + logValue = p.get("logging.level", logValue); + if (logValue.trim().equalsIgnoreCase("ALL")) { + logValue = "SQL"; + } + return Enum.valueOf(LogLevel.class, logValue.toUpperCase()); + } + + private NamingConvention createNamingConvention(PropertySource p) { + + NamingConvention nc = createInstance(p, NamingConvention.class, "namingconvention"); + if (nc == null) { + return null; + } + if (nc instanceof AbstractNamingConvention) { + AbstractNamingConvention anc = (AbstractNamingConvention) nc; + String v = p.get("namingConvention.useForeignKeyPrefix", null); + if (v != null) { + boolean useForeignKeyPrefix = Boolean.valueOf(v); + anc.setUseForeignKeyPrefix(useForeignKeyPrefix); + } + + String sequenceFormat = p.get("namingConvention.sequenceFormat", null); + if (sequenceFormat != null) { + anc.setSequenceFormat(sequenceFormat); + } + } + return nc; + } + + /** + * Build the list of classes from the comma delimited string. + * + * @param p + * the p + * + * @return the classes + */ + private ArrayList> getClasses(PropertySource p) { + + String classNames = p.get("classes", null); + if (classNames == null) { + + return null; + } + + ArrayList> classes = new ArrayList>(); + + String[] split = classNames.split("[ ,;]"); + for (int i = 0; i < split.length; i++) { + String cn = split[i].trim(); + if (cn.length() > 0 && !"class".equalsIgnoreCase(cn)) { + try { + classes.add(Class.forName(cn)); + } catch (ClassNotFoundException e) { + String msg = "Error registering class [" + cn + "] from [" + classNames + "]"; + throw new RuntimeException(msg, e); + } + } + } + return classes; + } + + private List getSearchJarsPackages(String searchPackages) { + + List hitList = new ArrayList(); + + if (searchPackages != null) { + + String[] entries = searchPackages.split("[ ,;]"); + for (int i = 0; i < entries.length; i++) { + hitList.add(entries[i].trim()); + } + } + return hitList; + } + +} diff --git a/src/main/java/com/avaje/ebean/config/TableName.java b/src/main/java/com/avaje/ebean/config/TableName.java new file mode 100644 index 000000000..ebfc55797 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/TableName.java @@ -0,0 +1,132 @@ +package com.avaje.ebean.config; + +/** + * TableName holds catalog, schema and table name. + * + * @author emcgreal + */ +public final class TableName { + + /** The catalog. */ + private String catalog; + + /** The schema. */ + private String schema; + + /** The name. */ + private String name; + + /** + * Construct with the given catalog schema and table name. + *

    + * Note the catalog and schema can be null. + *

    + */ + public TableName(String catalog, String schema, String name) { + super(); + this.catalog = catalog != null ? catalog.trim() : null; + this.schema = schema != null ? schema.trim() : null; + this.name = name != null ? name.trim() : null; + } + + /** + * Construct splitting the qualifiedTableName potentially into catalog, schema + * and name. + *

    + * The qualifiedTableName can take the form of catalog.schema.tableName and is + * split on the '.' period character. The catalog and schema are optional. + *

    + * + * @param qualifiedTableName + * the fully qualified table name using '.' between schema and table + * name etc (with catalog and schema optional). + */ + public TableName(String qualifiedTableName) { + String[] split = qualifiedTableName.split("\\."); + int len = split.length; + if (split.length > 3) { + String m = "Error splitting " + qualifiedTableName + ". Expecting at most 2 '.' characters"; + throw new RuntimeException(m); + } + if (len == 3) { + this.catalog = split[0]; + } + if (len >= 2) { + this.schema = split[len - 2]; + } + this.name = split[len - 1]; + } + + public String toString() { + return getQualifiedName(); + } + + /** + * Gets the catalog. + * + * @return the catalog + */ + public String getCatalog() { + return catalog; + } + + /** + * Gets the schema. + * + * @return the schema + */ + public String getSchema() { + return schema; + } + + /** + * Gets the name. + * + * @return the name + */ + public String getName() { + return name; + } + + /** + * Returns the qualified name in the form catalog.schema.name. + *

    + * Catalog and schema are optional. + *

    + * + * @return the qualified name + */ + public String getQualifiedName() { + + StringBuilder buffer = new StringBuilder(); + + // Add catalog + if (catalog != null) { + buffer.append(catalog); + } + + // Add schema + if (schema != null) { + if (buffer.length() > 0) { + buffer.append("."); + } + buffer.append(schema); + } + + if (buffer.length() > 0) { + buffer.append("."); + } + buffer.append(name); + + return buffer.toString(); + } + + /** + * Checks if is table name is valid i.e. it has at least a name. + * + * @return true, if is valid + */ + public boolean isValid() { + return name != null && name.length() > 0; + } +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/config/UnderscoreNamingConvention.java b/src/main/java/com/avaje/ebean/config/UnderscoreNamingConvention.java new file mode 100644 index 000000000..1ccb78c6b --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/UnderscoreNamingConvention.java @@ -0,0 +1,172 @@ +package com.avaje.ebean.config; + +/** + * Converts between Camel Case and Underscore based names for both table and + * column names (and is the default naming convention in Ebean). + * + * @author emcgreal + * @author rbygrave + */ +public class UnderscoreNamingConvention extends AbstractNamingConvention { + + /** Force toUnderscore to return in upper case. */ + private boolean forceUpperCase = false; + + /** The digits compressed. */ + private boolean digitsCompressed = true; + + /** + * Create with a given sequence format. + * + * @param sequenceFormat + * the sequence format + */ + public UnderscoreNamingConvention(String sequenceFormat) { + super(sequenceFormat); + } + + /** + * Create with a sequence format of "{table}_seq". + */ + public UnderscoreNamingConvention() { + super(); + } + + /** + * Returns the last part of the class name. + * + * @param beanClass + * the bean class + * + * @return the table name from class + */ + public TableName getTableNameByConvention(Class beanClass) { + + return new TableName(getCatalog(), + getSchema(), + toUnderscoreFromCamel(beanClass.getSimpleName())); + } + + /** + * Converts Camel case property name to underscore based column name. + * + * @return the column from property + */ + public String getColumnFromProperty(Class beanClass, String propertyName) {// Field + // field) + // { + + return toUnderscoreFromCamel(propertyName); + } + + /** + * Converts underscore based column name to Camel case property name. + * + * @param beanClass + * the bean class + * @param dbColumnName + * the db column name + * + * @return the property from column + */ + public String getPropertyFromColumn(Class beanClass, String dbColumnName) { + return toCamelFromUnderscore(dbColumnName); + } + + /** + * Return true if the result will be upper case. + *

    + * False if it will be lower case. + *

    + */ + public boolean isForceUpperCase() { + return forceUpperCase; + } + + /** + * Set to true to make the result upper case. + */ + public void setForceUpperCase(boolean forceUpperCase) { + this.forceUpperCase = forceUpperCase; + } + + /** + * Returns true if digits are compressed. + */ + public boolean isDigitsCompressed() { + return digitsCompressed; + } + + /** + * Sets to true for digits to be compressed (without a leading underscore). + */ + public void setDigitsCompressed(boolean digitsCompressed) { + this.digitsCompressed = digitsCompressed; + } + + /** + * Convert and return the string to underscore from camel case. + */ + protected String toUnderscoreFromCamel(String camelCase) { + + int lastUpper = -1; + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < camelCase.length(); i++) { + char c = camelCase.charAt(i); + + if ('_' == c) { + // Underscores should just be passed through + sb.append(c); + lastUpper = i; + } else if (Character.isDigit(c)) { + if (i > lastUpper + 1 && !digitsCompressed) { + sb.append("_"); + } + sb.append(c); + lastUpper = i; + + } else if (Character.isUpperCase(c)) { + if (i > lastUpper + 1) { + sb.append("_"); + } + sb.append(Character.toLowerCase(c)); + lastUpper = i; + + } else { + sb.append(c); + } + } + String ret = sb.toString(); + if (forceUpperCase) { + ret = ret.toUpperCase(); + } + return ret; + } + + /** + * To camel from underscore. + * + * @param underscore + * the underscore + * + * @return the string + */ + protected String toCamelFromUnderscore(String underscore) { + + StringBuffer result = new StringBuffer(); + String[] vals = underscore.split("_"); + + for (int i = 0; i < vals.length; i++) { + String lower = vals[i].toLowerCase(); + if (i > 0) { + char c = Character.toUpperCase(lower.charAt(0)); + result.append(c); + result.append(lower.substring(1)); + } else { + result.append(lower); + } + } + + return result.toString(); + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/AbstractDbEncrypt.java b/src/main/java/com/avaje/ebean/config/dbplatform/AbstractDbEncrypt.java new file mode 100644 index 000000000..980863bea --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/AbstractDbEncrypt.java @@ -0,0 +1,77 @@ +package com.avaje.ebean.config.dbplatform; + +import java.sql.Types; + +/** + * Base type for DB platform specific Encryption. + *

    + * DB specific classes that extend this need to set their specific encryption + * functions for varchar, date and timestamp. If they are left null then that is + * treated as though that data type can not be encrypted in the DB and will + * instead use java client encryption. + *

    + * + * @author rbygrave + */ +public abstract class AbstractDbEncrypt implements DbEncrypt { + + /** + * The encryption function for all String types (VARCHAR, CLOB, LONGVARCHAR, + * CHAR). + */ + protected DbEncryptFunction varcharEncryptFunction; + + /** + * The encryption function for all Date types (java.sql.Date, Joda Date + * types). + */ + protected DbEncryptFunction dateEncryptFunction; + + /** + * The encryption function for all Timestamp types (java.sql.Timestamp, + * java.util.Date, java.util.Calendar, Joda DateTime types etc). + */ + protected DbEncryptFunction timestampEncryptFunction; + + /** + * Return the DB encryption function for the given JDBC type. + *

    + * Null is returned if DB encryption of the type is not supported. + *

    + */ + public DbEncryptFunction getDbEncryptFunction(int jdbcType) { + switch (jdbcType) { + case Types.VARCHAR: + return varcharEncryptFunction; + case Types.CLOB: + return varcharEncryptFunction; + case Types.CHAR: + return varcharEncryptFunction; + case Types.LONGVARCHAR: + return varcharEncryptFunction; + + case Types.DATE: + return dateEncryptFunction; + + case Types.TIMESTAMP: + return timestampEncryptFunction; + + default: + return null; + } + } + + /** + * Return the DB stored type for encrypted properties. + */ + public int getEncryptDbType() { + return Types.VARBINARY; + } + + /** + * Generally encrypt function binding the data before the key (except h2). + */ + public boolean isBindEncryptDataFirst() { + return true; + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DB2Platform.java b/src/main/java/com/avaje/ebean/config/dbplatform/DB2Platform.java new file mode 100644 index 000000000..a5abf32bd --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DB2Platform.java @@ -0,0 +1,56 @@ +package com.avaje.ebean.config.dbplatform; + +import java.sql.Types; + +import javax.sql.DataSource; + +import com.avaje.ebean.BackgroundExecutor; + +/** + * DB2 specific platform. + */ +public class DB2Platform extends DatabasePlatform { + + public DB2Platform() { + super(); + this.name = "db2"; + + // only support getGeneratedKeys with non-batch JDBC + // so generally use SEQUENCE instead for H2 + this.dbIdentity.setSupportsGetGeneratedKeys(true); + this.dbIdentity.setIdType(IdType.IDENTITY); + this.dbIdentity.setSupportsSequence(true); + + this.openQuote = "\""; + this.closeQuote = "\""; + + booleanDbType = Types.INTEGER; + dbTypeMap.put(Types.BOOLEAN, new DbType("smallint default 0")); + dbTypeMap.put(Types.INTEGER, new DbType("integer")); + dbTypeMap.put(Types.BIGINT, new DbType("bigint")); + dbTypeMap.put(Types.REAL, new DbType("float")); + dbTypeMap.put(Types.DOUBLE, new DbType("float")); + dbTypeMap.put(Types.SMALLINT, new DbType("smallint")); + dbTypeMap.put(Types.TINYINT, new DbType("smallint")); + dbTypeMap.put(Types.DECIMAL, new DbType("decimal", 15)); + + this.dbDdlSyntax.setIdentity("generated by default as identity"); + + // this.dbDdlSyntax.setDropIfExists("if exists"); + // this.dbDdlSyntax.setDisableReferentialIntegrity("SET REFERENTIAL_INTEGRITY FALSE"); + // this.dbDdlSyntax.setEnableReferentialIntegrity("SET REFERENTIAL_INTEGRITY TRUE"); + // this.dbDdlSyntax.setForeignKeySuffix("on delete restrict on update restrict"); + } + + /** + * Return a DB2 specific sequence IdGenerator that supports batch fetching + * sequence values. + */ + @Override + public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, + DataSource ds, String seqName, int batchSize) { + + return new DB2SequenceIdGenerator(be, ds, seqName, batchSize); + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DB2SequenceIdGenerator.java b/src/main/java/com/avaje/ebean/config/dbplatform/DB2SequenceIdGenerator.java new file mode 100644 index 000000000..b76d5a52b --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DB2SequenceIdGenerator.java @@ -0,0 +1,33 @@ +package com.avaje.ebean.config.dbplatform; + +import javax.sql.DataSource; + +import com.avaje.ebean.BackgroundExecutor; + +/** + * DB2 specific sequence Id Generator. + */ +public class DB2SequenceIdGenerator extends SequenceIdGenerator { + + private final String baseSql; + private final String unionBaseSql; + + /** + * Construct given a dataSource and sql to return the next sequence value. + */ + public DB2SequenceIdGenerator(BackgroundExecutor be, DataSource ds, String seqName, int batchSize) { + super(be, ds, seqName, batchSize); + this.baseSql = "select nextval for " + seqName; + this.unionBaseSql = " union " + baseSql; + } + + public String getSql(int batchSize) { + + StringBuilder sb = new StringBuilder(); + sb.append(baseSql); + for (int i = 1; i < batchSize; i++) { + sb.append(unionBaseSql); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java b/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java new file mode 100644 index 000000000..4ebad2729 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java @@ -0,0 +1,278 @@ +package com.avaje.ebean.config.dbplatform; + +import java.sql.Types; + +import javax.sql.DataSource; + +import com.avaje.ebean.BackgroundExecutor; +import com.avaje.ebean.Query; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Database platform specific settings. + */ +public class DatabasePlatform { + + /** The Constant logger. */ + private static final Logger logger = LoggerFactory.getLogger(DatabasePlatform.class); + + /** The open quote used by quoted identifiers. */ + protected String openQuote = "\""; + + /** The close quote used by quoted identifiers. */ + protected String closeQuote = "\""; + + /** For limit/offset, row_number etc limiting of SQL queries. */ + protected SqlLimiter sqlLimiter = new LimitOffsetSqlLimiter(); + + /** Mapping of JDBC to Database types. */ + protected DbTypeMap dbTypeMap = new DbTypeMap(); + + /** DB specific DDL syntax. */ + protected DbDdlSyntax dbDdlSyntax = new DbDdlSyntax(); + + /** Defines DB identity/sequence features. */ + protected DbIdentity dbIdentity = new DbIdentity(); + + /** The JDBC type to map booleans to (by default). */ + protected int booleanDbType = Types.BOOLEAN; + + /** The JDBC type to map Blob to. */ + protected int blobDbType = Types.BLOB; + + /** The JDBC type to map Clob to. */ + protected int clobDbType = Types.CLOB; + + /** For Oracle treat empty strings as null. */ + protected boolean treatEmptyStringsAsNull; + + /** The name. */ + protected String name = "generic"; + + /** + * Use a BackTick ` at the beginning and end of table or column names that you + * want to use quoted identifiers for. The backticks get converted to the + * appropriate characters in convertQuotedIdentifiers + */ + private static final char BACK_TICK = '`'; + + protected DbEncrypt dbEncrypt; + + protected boolean idInExpandedForm; + + protected boolean selectCountWithAlias; + + /** + * Instantiates a new database platform. + */ + public DatabasePlatform() { + } + + /** + * Return the name of the DatabasePlatform. + *

    + * "generic" is returned when no specific database platform has been set or + * found. + *

    + * + * @return the name + */ + public String getName() { + return name; + } + + /** + * Return a DB Sequence based IdGenerator. + * + * @param be + * the BackgroundExecutor that can be used to load the sequence if + * desired + * @param ds + * the DataSource + * @param seqName + * the name of the sequence + * @param batchSize + * the number of sequences that should be loaded + */ + public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, + String seqName, int batchSize) { + + return null; + } + + /** + * Return the DbEncrypt handler for this DB platform. + */ + public DbEncrypt getDbEncrypt() { + return dbEncrypt; + } + + /** + * Set the DbEncrypt handler for this DB platform. + */ + public void setDbEncrypt(DbEncrypt dbEncrypt) { + this.dbEncrypt = dbEncrypt; + } + + /** + * Return the mapping of JDBC to DB types. + * + * @return the db type map + */ + public DbTypeMap getDbTypeMap() { + return dbTypeMap; + } + + /** + * Return the DDL syntax for this platform. + * + * @return the db ddl syntax + */ + public DbDdlSyntax getDbDdlSyntax() { + return dbDdlSyntax; + } + + /** + * Return the close quote for quoted identifiers. + * + * @return the close quote + */ + public String getCloseQuote() { + return closeQuote; + } + + /** + * Return the open quote for quoted identifiers. + * + * @return the open quote + */ + public String getOpenQuote() { + return openQuote; + } + + /** + * Return the JDBC type used to store booleans. + * + * @return the boolean db type + */ + public int getBooleanDbType() { + return booleanDbType; + } + + /** + * Return the data type that should be used for Blob. + *

    + * This is typically Types.BLOB but for Postgres is Types.LONGVARBINARY for + * example. + *

    + */ + public int getBlobDbType() { + return blobDbType; + } + + /** + * Return the data type that should be used for Clob. + *

    + * This is typically Types.CLOB but for Postgres is Types.VARCHAR. + *

    + */ + public int getClobDbType() { + return clobDbType; + } + + /** + * Return true if empty strings should be treated as null. + * + * @return true, if checks if is treat empty strings as null + */ + public boolean isTreatEmptyStringsAsNull() { + return treatEmptyStringsAsNull; + } + + /** + * Return true if a compound ID in (...) type expression needs to be in + * expanded form of (a=? and b=?) or (a=? and b=?) or ... rather than (a,b) in + * ((?,?),(?,?),...); + */ + public boolean isIdInExpandedForm() { + return idInExpandedForm; + } + + /** + * Return the DB identity/sequence features for this platform. + * + * @return the db identity + */ + public DbIdentity getDbIdentity() { + return dbIdentity; + } + + /** + * Return the SqlLimiter used to apply additional sql around a query to limit + * its results. + *

    + * Basically add the clauses for limit/offset, rownum, row_number(). + *

    + * + * @return the sql limiter + */ + public SqlLimiter getSqlLimiter() { + return sqlLimiter; + } + + /** + * Convert backticks to the platform specific open quote and close quote + * + *

    + * Specific plugins may implement this method to cater for platform specific + * naming rules. + *

    + * + * @param dbName + * the db name + * + * @return the string + */ + public String convertQuotedIdentifiers(String dbName) { + // Ignore null values e.g. schema name or catalog + if (dbName != null && dbName.length() > 0) { + if (dbName.charAt(0) == BACK_TICK) { + if (dbName.charAt(dbName.length() - 1) == BACK_TICK) { + + String quotedName = getOpenQuote(); + quotedName += dbName.substring(1, dbName.length() - 1); + quotedName += getCloseQuote(); + + return quotedName; + + } else { + logger.error("Missing backquote on [" + dbName + "]"); + } + } + } + return dbName; + } + + /** + * Set to true if select count against anonymous view requires an alias. + */ + public boolean isSelectCountWithAlias() { + return selectCountWithAlias; + } + + public String completeSql(String sql, Query query) { + if (Boolean.TRUE.equals(query.isForUpdate())) { + sql = withForUpdate(sql); + } + + return sql; + } + + protected String withForUpdate(String sql) { + // silently assume the database does not support the "for update" clause. + logger.info("it seems your database does not support the 'for update' clause"); + + return sql; + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DbDdlSyntax.java b/src/main/java/com/avaje/ebean/config/dbplatform/DbDdlSyntax.java new file mode 100644 index 000000000..f71f80c36 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbDdlSyntax.java @@ -0,0 +1,277 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Used to support DB specific syntax for DDL generation. + */ +public class DbDdlSyntax { + + private boolean renderIndexForFkey = true; + + private boolean inlinePrimaryKeyConstraint = false; + + private boolean addOneToOneUniqueContraint = false; + + private int maxConstraintNameLength = 32; + + private int columnNameWidth = 25; + + private String dropTableCascade; + private String dropIfExists; + + private String newLine = "\r\n"; + + private String identity = "auto_increment"; + + private String pkPrefix = "pk_"; + + private String disableReferentialIntegrity; + private String enableReferentialIntegrity; + + private String foreignKeySuffix; + + /** + * Return the primary key name for a given bean descriptor. + */ + public String getPrimaryKeyName(String tableName) { + + String pk = pkPrefix + tableName; + if (pk.length() > maxConstraintNameLength) { + // need to trim the primary key name + pk = pk.substring(0, maxConstraintNameLength); + } + return pk; + } + + /** + * Return the identity clause for DB's that have identities. + */ + public String getIdentity() { + return identity; + } + + /** + * Set the identity clause. + */ + public void setIdentity(String identity) { + this.identity = identity; + } + + /** + * Return the width for padding whitespace after column names. + */ + public int getColumnNameWidth() { + return columnNameWidth; + } + + /** + * Set the amount of padding to write after the column name. + */ + public void setColumnNameWidth(int columnNameWidth) { + this.columnNameWidth = columnNameWidth; + } + + /** + * Return the new line character. + */ + public String getNewLine() { + return newLine; + } + + /** + * Set the new line character. + */ + public void setNewLine(String newLine) { + this.newLine = newLine; + } + + /** + * Return the prefix used in naming primary keys. + */ + public String getPkPrefix() { + return pkPrefix; + } + + /** + * Set the prefix used in naming primary keys. + */ + public void setPkPrefix(String pkPrefix) { + this.pkPrefix = pkPrefix; + } + + /** + * Return the DB specific command to disable referential integrity + */ + public String getDisableReferentialIntegrity() { + return disableReferentialIntegrity; + } + + /** + * Set the DB specific command to disable referential integrity + */ + public void setDisableReferentialIntegrity(String disableReferentialIntegrity) { + this.disableReferentialIntegrity = disableReferentialIntegrity; + } + + /** + * Return the DB specific command to enable referential integrity + */ + public String getEnableReferentialIntegrity() { + return enableReferentialIntegrity; + } + + /** + * Set the DB specific command to enable referential integrity + */ + public void setEnableReferentialIntegrity(String enableReferentialIntegrity) { + this.enableReferentialIntegrity = enableReferentialIntegrity; + } + + /** + * Return true if indexes should be created for the foreign keys. + */ + public boolean isRenderIndexForFkey() { + return renderIndexForFkey; + } + + /** + * Set whether indexes should be created for the foreign keys. + */ + public void setRenderIndexForFkey(boolean renderIndexForFkey) { + this.renderIndexForFkey = renderIndexForFkey; + } + + /** + * Typically returns IF EXISTS (if that is supported by the database platform) + * or null. + */ + public String getDropIfExists() { + return dropIfExists; + } + + /** + * Set the IF EXISTS clause for dropping tables if that is supported by the + * database platform. + */ + public void setDropIfExists(String dropIfExists) { + this.dropIfExists = dropIfExists; + } + + /** + * Return the cascade option for the drop table command. + */ + public String getDropTableCascade() { + return dropTableCascade; + } + + /** + * Set the cascade option for the drop table command. + */ + public void setDropTableCascade(String dropTableCascade) { + this.dropTableCascade = dropTableCascade; + } + + /** + * Return the foreign key suffix. + */ + public String getForeignKeySuffix() { + return foreignKeySuffix; + } + + /** + * Set the foreign key suffix. + */ + public void setForeignKeySuffix(String foreignKeySuffix) { + this.foreignKeySuffix = foreignKeySuffix; + } + + /** + * Return the maximum length that constraint names can be for this database. + */ + public int getMaxConstraintNameLength() { + return maxConstraintNameLength; + } + + /** + * Set the maximum length that constraint names can be for this database. + */ + public void setMaxConstraintNameLength(int maxFkeyLength) { + this.maxConstraintNameLength = maxFkeyLength; + } + + /** + * Return true if imported side of OneToOne's should have unique constraints + * generated. + */ + public boolean isAddOneToOneUniqueContraint() { + return addOneToOneUniqueContraint; + } + + /** + * Set to false for DB's that don't want both a unique and index on the + * imported OneToOne. + */ + public void setAddOneToOneUniqueContraint(boolean addOneToOneUniqueContraint) { + this.addOneToOneUniqueContraint = addOneToOneUniqueContraint; + } + + /** + * Return true if primary key constraints should be inlined when they are a + * single column. + */ + public boolean isInlinePrimaryKeyConstraint() { + return inlinePrimaryKeyConstraint; + } + + /** + * Set whether to inline primary key constraints. + */ + public void setInlinePrimaryKeyConstraint(boolean inlinePrimaryKeyConstraint) { + this.inlinePrimaryKeyConstraint = inlinePrimaryKeyConstraint; + } + + public String getIndexName(String table, String propName, int ixCount) { + + StringBuilder buffer = new StringBuilder(); + buffer.append("ix_"); + buffer.append(table); + buffer.append("_"); + buffer.append(propName); + + addSuffix(buffer, ixCount); + + return buffer.toString(); + } + + public String getForeignKeyName(String table, String propName, int fkCount) { + + StringBuilder buffer = new StringBuilder(); + buffer.append("fk_"); + buffer.append(table); + buffer.append("_"); + buffer.append(propName); + + addSuffix(buffer, fkCount); + + return buffer.toString(); + } + + /** + * Adds the suffix. + * + * @param buffer + * the buffer + * @param count + * the count + */ + protected void addSuffix(StringBuilder buffer, int count) { + final String suffixNr = Integer.toString(count); + final int suffixLen = suffixNr.length() + 1; + + if (buffer.length() + suffixLen > maxConstraintNameLength) { + buffer.setLength(maxConstraintNameLength - suffixLen); + } + buffer.append("_"); + buffer.append(suffixNr); + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DbEncrypt.java b/src/main/java/com/avaje/ebean/config/dbplatform/DbEncrypt.java new file mode 100644 index 000000000..ae2ed8792 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbEncrypt.java @@ -0,0 +1,41 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Defines DB encryption support for encrypting and decrypting data using DB + * encryption features. + *

    + * As an alternative to using DB encryption you can encrypt/decrypt in java via + * a special ScalarType but this has the limitation that you can't include that + * property in query where clauses. + *

    + * + * @author rbygrave + */ +public interface DbEncrypt { + + // /** + // * Return the SQL for decrypting a column returning a VARCHAR. + // */ + // public String getDecryptSql(String columnWithTableAlias); + // + // /** + // * Return the DB function with bind variables used to encrypt a VARCHAR + // * value. + // */ + // public String getEncryptBindSql(); + + public DbEncryptFunction getDbEncryptFunction(int jdbcType); + + /** + * Return the DB type that encrypted Strings are stored in. + *

    + * This is VARCHAR for MySql and VARBINARY for most others. + *

    + */ + public int getEncryptDbType(); + + /** + * Return true if the DB encrypt function binds the data before the key. + */ + public boolean isBindEncryptDataFirst(); +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DbEncryptFunction.java b/src/main/java/com/avaje/ebean/config/dbplatform/DbEncryptFunction.java new file mode 100644 index 000000000..4955633e1 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbEncryptFunction.java @@ -0,0 +1,15 @@ +package com.avaje.ebean.config.dbplatform; + +public interface DbEncryptFunction { + + /** + * Return the SQL for decrypting a column returning a VARCHAR. + */ + public String getDecryptSql(String columnWithTableAlias); + + /** + * Return the DB function with bind variables used to encrypt a VARCHAR value. + */ + public String getEncryptBindSql(); + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DbIdentity.java b/src/main/java/com/avaje/ebean/config/dbplatform/DbIdentity.java new file mode 100644 index 000000000..2cdc5fd6f --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbIdentity.java @@ -0,0 +1,117 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Defines the identity/sequence behaviour for the database. + */ +public class DbIdentity { + + /** + * Set if this DB supports sequences. Note some DB's support both Sequences + * and Identity. + */ + private boolean supportsSequence; + private boolean supportsIdentity; + + private boolean supportsGetGeneratedKeys; + + private String selectLastInsertedIdTemplate; + + private IdType idType = IdType.IDENTITY; + + public DbIdentity() { + } + + /** + * Return true if GetGeneratedKeys is supported. + *

    + * GetGeneratedKeys required to support JDBC batching transparently. + *

    + */ + public boolean isSupportsGetGeneratedKeys() { + return supportsGetGeneratedKeys; + } + + /** + * Set if GetGeneratedKeys is supported. + */ + public void setSupportsGetGeneratedKeys(boolean supportsGetGeneratedKeys) { + this.supportsGetGeneratedKeys = supportsGetGeneratedKeys; + } + + /** + * Return the SQL query to find the SelectLastInsertedId. + *

    + * This should only be set on databases that don't support GetGeneratedKeys. + *

    + */ + public String getSelectLastInsertedId(String table) { + if (selectLastInsertedIdTemplate == null) { + return null; + } + return selectLastInsertedIdTemplate.replace("{table}", table); + } + + /** + * Set the template used to build the SQL query to return the LastInsertedId. + *

    + * The template can contain "{table}" where the table name should be include + * in the sql query. + *

    + *

    + * This should only be set on databases that don't support GetGeneratedKeys. + *

    + */ + public void setSelectLastInsertedIdTemplate(String selectLastInsertedIdTemplate) { + this.selectLastInsertedIdTemplate = selectLastInsertedIdTemplate; + } + + /** + * Return true if the database supports sequences. + */ + public boolean isSupportsSequence() { + return supportsSequence; + } + + /** + * Set to true if the database supports sequences. Generally this also means + * you want to set the default IdType to sequence (some DB's support both + * sequences and identity). + */ + public void setSupportsSequence(boolean supportsSequence) { + this.supportsSequence = supportsSequence; + } + + /** + * Return true if this DB platform supports identity (autoincrement). + */ + public boolean isSupportsIdentity() { + return supportsIdentity; + } + + /** + * Set to true if this DB platform supports identity (autoincrement). + */ + public void setSupportsIdentity(boolean supportsIdentity) { + this.supportsIdentity = supportsIdentity; + } + + /** + * Return the default ID generation type that should be used. This should be + * either SEQUENCE or IDENTITY (aka Autoincrement). + *

    + * Note: Id properties of type UUID automatically get a UUID generator + * assigned to them. + *

    + */ + public IdType getIdType() { + return idType; + } + + /** + * Set the default ID generation type that should be used. + */ + public void setIdType(IdType idType) { + this.idType = idType; + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DbType.java b/src/main/java/com/avaje/ebean/config/dbplatform/DbType.java new file mode 100644 index 000000000..7b31b43bb --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbType.java @@ -0,0 +1,108 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Represents a DB type with name, length, precision, and scale. + *

    + * The length is for VARCHAR types and precision/scale for DECIMAL types. + *

    + */ +public class DbType { + + /** + * The data type name (VARCHAR, INTEGER ...) + */ + private final String name; + + /** + * The default length or precision. + */ + private final int defaultLength; + + /** + * The default scale (decimal). + */ + private final int defaultScale; + + /** + * Set to true if the type should never have a length or scale. + */ + private final boolean canHaveLength; + + /** + * Construct with no length or scale. + */ + public DbType(String name) { + this(name, 0, 0); + } + + /** + * Construct with a given length. + */ + public DbType(String name, int defaultLength) { + this(name, defaultLength, 0); + } + + /** + * Construct for Decimal with precision and scale. + */ + public DbType(String name, int defaultPrecision, int defaultScale) { + this.name = name; + this.defaultLength = defaultPrecision; + this.defaultScale = defaultScale; + this.canHaveLength = true; + } + + /** + * Use with canHaveLength=false for types that should never have a length. + * + * @param name + * the type name + * @param canHaveLength + * set this to false for type that should never have a length + */ + public DbType(String name, boolean canHaveLength) { + this.name = name; + this.defaultLength = 0; + this.defaultScale = 0; + this.canHaveLength = canHaveLength; + } + + /** + * Return the type for a specific property that incorporates the name, length, + * precision and scale. + *

    + * The deployLength and deployScale are for the property we are rendering the + * DB type for. + *

    + * + * @param deployLength + * the length or precision defined by deployment on a specific + * property. + * @param deployScale + * the scale defined by deployment on a specific property. + */ + public String renderType(int deployLength, int deployScale) { + + StringBuilder sb = new StringBuilder(); + sb.append(name); + + if (canHaveLength) { + // see if there is a precision/scale to add (or not) + int len = deployLength != 0 ? deployLength : defaultLength; + + if (len > 0) { + sb.append("("); + sb.append(len); + int scale = deployScale != 0 ? deployScale : defaultScale; + if (scale > 0) { + sb.append(","); + sb.append(scale); + } + sb.append(")"); + } + } + + return sb.toString(); + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java b/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java new file mode 100644 index 000000000..7cc04c2d0 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DbTypeMap.java @@ -0,0 +1,70 @@ +package com.avaje.ebean.config.dbplatform; + +import java.sql.Types; +import java.util.HashMap; +import java.util.Map; + +/** + * Used to map bean property types to DB specific types for DDL generation. + */ +public class DbTypeMap { + + private final Map typeMap = new HashMap(); + + public DbTypeMap() { + loadDefaults(); + } + + /** + * Load the standard types. These can be overridden by DB specific platform. + */ + private void loadDefaults() { + + put(Types.BOOLEAN, new DbType("boolean")); + put(Types.BIT, new DbType("bit")); + + put(Types.INTEGER, new DbType("integer")); + put(Types.BIGINT, new DbType("bigint")); + put(Types.REAL, new DbType("float")); + put(Types.DOUBLE, new DbType("double")); + put(Types.SMALLINT, new DbType("smallint")); + put(Types.TINYINT, new DbType("tinyint")); + put(Types.DECIMAL, new DbType("decimal", 38)); + + put(Types.VARCHAR, new DbType("varchar", 255)); + put(Types.CHAR, new DbType("char", 1)); + + put(Types.BLOB, new DbType("blob")); + put(Types.CLOB, new DbType("clob")); + put(Types.LONGVARBINARY, new DbType("longvarbinary")); + put(Types.LONGVARCHAR, new DbType("lonvarchar")); + put(Types.VARBINARY, new DbType("varbinary", 255)); + put(Types.BINARY, new DbType("binary", 255)); + + put(Types.DATE, new DbType("date")); + put(Types.TIME, new DbType("time")); + put(Types.TIMESTAMP, new DbType("timestamp")); + + } + + /** + * Override the type for a given JDBC type. + */ + public void put(int jdbcType, DbType dbType) { + typeMap.put(Integer.valueOf(jdbcType), dbType); + } + + /** + * Return the type for a given jdbc type. + */ + public DbType get(int jdbcType) { + + DbType dbType = typeMap.get(Integer.valueOf(jdbcType)); + if (dbType == null) { + String m = "No DB type for JDBC type " + jdbcType; + throw new RuntimeException(m); + } + + return dbType; + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/H2DbEncrypt.java b/src/main/java/com/avaje/ebean/config/dbplatform/H2DbEncrypt.java new file mode 100644 index 000000000..63c406712 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/H2DbEncrypt.java @@ -0,0 +1,48 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * H2 encryption support via encrypt decrypt function. + * + * @author rbygrave + */ +public class H2DbEncrypt extends AbstractDbEncrypt { + + public H2DbEncrypt() { + this.varcharEncryptFunction = new H2VarcharFunction(); + this.dateEncryptFunction = new H2DateFunction(); + } + + /** + * For H2 encrypt function returns false binding the key before the data. + */ + public boolean isBindEncryptDataFirst() { + return false; + } + + private static class H2VarcharFunction implements DbEncryptFunction { + + public String getDecryptSql(String columnWithTableAlias) { + // Hmmm, this looks ugly - checking with H2 Database folks. + return "TRIM(CHAR(0) FROM UTF8TOSTRING(DECRYPT('AES', STRINGTOUTF8(?), " + + columnWithTableAlias + ")))"; + } + + public String getEncryptBindSql() { + return "ENCRYPT('AES', STRINGTOUTF8(?), STRINGTOUTF8(?))"; + } + + } + + private static class H2DateFunction implements DbEncryptFunction { + + public String getDecryptSql(String columnWithTableAlias) { + return "PARSEDATETIME(TRIM(CHAR(0) FROM UTF8TOSTRING(DECRYPT('AES', STRINGTOUTF8(?), " + + columnWithTableAlias + "))),'yyyyMMdd')"; + } + + public String getEncryptBindSql() { + return "ENCRYPT('AES', STRINGTOUTF8(?), STRINGTOUTF8(FORMATDATETIME(?,'yyyyMMdd')))"; + } + + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/H2Platform.java b/src/main/java/com/avaje/ebean/config/dbplatform/H2Platform.java new file mode 100644 index 000000000..62572ad77 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/H2Platform.java @@ -0,0 +1,56 @@ +package com.avaje.ebean.config.dbplatform; + +import com.avaje.ebean.BackgroundExecutor; +import com.avaje.ebean.config.GlobalProperties; + +import javax.sql.DataSource; + +/** + * H2 specific platform. + */ +public class H2Platform extends DatabasePlatform { + + public H2Platform() { + super(); + this.name = "h2"; + this.dbEncrypt = new H2DbEncrypt(); + + // only support getGeneratedKeys with non-batch JDBC + // so generally use SEQUENCE instead of IDENTITY for H2 + boolean useIdentity = GlobalProperties.getBoolean("ebean.h2platform.useIdentity", false); + + IdType idType = useIdentity ? IdType.IDENTITY : IdType.SEQUENCE; + this.dbIdentity.setIdType(idType); + + this.dbIdentity.setSupportsGetGeneratedKeys(true); + this.dbIdentity.setSupportsSequence(true); + this.dbIdentity.setSupportsIdentity(true); + + this.openQuote = "\""; + this.closeQuote = "\""; + + // H2 data types match default JDBC types + // so no changes to dbTypeMap required + + this.dbDdlSyntax.setDropIfExists("if exists"); + this.dbDdlSyntax.setDisableReferentialIntegrity("SET REFERENTIAL_INTEGRITY FALSE"); + this.dbDdlSyntax.setEnableReferentialIntegrity("SET REFERENTIAL_INTEGRITY TRUE"); + this.dbDdlSyntax.setForeignKeySuffix("on delete restrict on update restrict"); + } + + /** + * Return a H2 specific sequence IdGenerator that supports batch fetching + * sequence values. + */ + @Override + public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, + String seqName, int batchSize) { + + return new H2SequenceIdGenerator(be, ds, seqName, batchSize); + } + + @Override + protected String withForUpdate(String sql) { + return sql + " for update"; + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/H2SequenceIdGenerator.java b/src/main/java/com/avaje/ebean/config/dbplatform/H2SequenceIdGenerator.java new file mode 100644 index 000000000..7547746f3 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/H2SequenceIdGenerator.java @@ -0,0 +1,33 @@ +package com.avaje.ebean.config.dbplatform; + +import javax.sql.DataSource; + +import com.avaje.ebean.BackgroundExecutor; + +/** + * H2 specific sequence Id Generator. + */ +public class H2SequenceIdGenerator extends SequenceIdGenerator { + + private final String baseSql; + private final String unionBaseSql; + + /** + * Construct given a dataSource and sql to return the next sequence value. + */ + public H2SequenceIdGenerator(BackgroundExecutor be, DataSource ds, String seqName, int batchSize) { + super(be, ds, seqName, batchSize); + this.baseSql = "select " + seqName + ".nextval"; + this.unionBaseSql = " union " + baseSql; + } + + public String getSql(int batchSize) { + + StringBuilder sb = new StringBuilder(); + sb.append(baseSql); + for (int i = 1; i < batchSize; i++) { + sb.append(unionBaseSql); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/HsqldbPlatform.java b/src/main/java/com/avaje/ebean/config/dbplatform/HsqldbPlatform.java new file mode 100644 index 000000000..c2c90822d --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/HsqldbPlatform.java @@ -0,0 +1,56 @@ +package com.avaje.ebean.config.dbplatform; + +import java.sql.Types; + +import javax.sql.DataSource; + +import com.avaje.ebean.BackgroundExecutor; +import com.avaje.ebean.config.GlobalProperties; + +/** + * H2 specific platform. + */ +public class HsqldbPlatform extends DatabasePlatform { + + public HsqldbPlatform() { + super(); + this.name = "hsqldb"; + this.dbEncrypt = new H2DbEncrypt(); + + // only support getGeneratedKeys with non-batch JDBC + // so generally use SEQUENCE instead of IDENTITY for H2 + boolean useIdentity = GlobalProperties.getBoolean("ebean.hsqldb.useIdentity", true); + + IdType idType = useIdentity ? IdType.IDENTITY : IdType.SEQUENCE; + this.dbIdentity.setIdType(idType); + + this.dbIdentity.setSupportsGetGeneratedKeys(true); + this.dbIdentity.setSupportsSequence(true); + this.dbIdentity.setSupportsIdentity(true); + + this.openQuote = "\""; + this.closeQuote = "\""; + + // H2 data types match default JDBC types + // so no changes to dbTypeMap required + dbTypeMap.put(Types.INTEGER, new DbType("integer", false)); + + this.dbDdlSyntax.setDropIfExists("if exists"); + this.dbDdlSyntax.setDisableReferentialIntegrity("SET DATABASE REFERENTIAL INTEGRITY FALSE"); + this.dbDdlSyntax.setEnableReferentialIntegrity("SET DATABASE REFERENTIAL INTEGRITY TRUE"); + this.dbDdlSyntax.setForeignKeySuffix("on delete restrict on update restrict"); + this.dbDdlSyntax.setIdentity("GENERATED BY DEFAULT AS IDENTITY (START WITH 1) "); + } + + /** + * Return a H2 specific sequence IdGenerator that supports batch fetching + * sequence values. + */ + @Override + public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, + DataSource ds, String seqName, int batchSize) { + + return new H2SequenceIdGenerator(be, ds, seqName, batchSize); + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/IdGenerator.java b/src/main/java/com/avaje/ebean/config/dbplatform/IdGenerator.java new file mode 100644 index 000000000..f366d89aa --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/IdGenerator.java @@ -0,0 +1,50 @@ +package com.avaje.ebean.config.dbplatform; + +import com.avaje.ebean.Transaction; + +/** + * Generates unique id's for objects. This occurs prior to the actual insert. + *

    + * Note that many databases have sequences or auto increment features. These can + * be used rather than an IdGenerator and are different in that they occur + * during an insert. IdGenerator is used to generate an id BEFORE the + * actual insert. + *

    + */ +public interface IdGenerator { + + /** + * The name of the default UUID generator. + */ + public static final String AUTO_UUID = "auto.uuid"; + + /** + * Return the name of the IdGenerator. For sequences this is the sequence + * name. + */ + public String getName(); + + /** + * Return true if this is a DB sequence. + */ + public boolean isDbSequence(); + + /** + * return the next unique identity value. + *

    + * Note the transaction passed in can be null. + *

    + */ + public Object nextId(Transaction transaction); + + /** + * Is called prior to inserting OneToMany's as an indication that a number of + * beans are likely to need id's shortly. + *

    + * Can be used as a performance optimisation to prefetch a number of Id's. + * Especially when the allocateSize is very large. + *

    + */ + public void preAllocateIds(int allocateSize); + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/IdType.java b/src/main/java/com/avaje/ebean/config/dbplatform/IdType.java new file mode 100644 index 000000000..a3203315f --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/IdType.java @@ -0,0 +1,31 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * The types of Identity generation that can be defined. + */ +public enum IdType { + + /** + * Use a Database Identity (autoincrement) to generate the identity. + */ + IDENTITY, + + /** + * Use a Database sequence to generate the identity. + *

    + * Note: Some databases support getGeneratedKeys with sequences and this then + * does not involve an extra statement to return the id. + *

    + */ + SEQUENCE, + + /** + * Use an IdGenerator to generate the identity (prior to insert). + *

    + * Note: There is a IdGenerator for UUID's and it is automatically assigned to + * id properties of type UUID. + *

    + */ + GENERATOR; + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/LimitOffsetSqlLimiter.java b/src/main/java/com/avaje/ebean/config/dbplatform/LimitOffsetSqlLimiter.java new file mode 100644 index 000000000..0af060b93 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/LimitOffsetSqlLimiter.java @@ -0,0 +1,48 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Adds LIMIT OFFSET clauses to a SQL query. + */ +public class LimitOffsetSqlLimiter implements SqlLimiter { + + /** + * LIMIT keyword. + */ + private static final String LIMIT = "limit"; + + /** + * OFFSET keyword. + */ + private static final String OFFSET = "offset"; + + public SqlLimitResponse limit(SqlLimitRequest request) { + + StringBuilder sb = new StringBuilder(512); + sb.append("select "); + if (request.isDistinct()) { + sb.append("distinct "); + } + + sb.append(request.getDbSql()); + + int firstRow = request.getFirstRow(); + int maxRows = request.getMaxRows(); + if (maxRows > 0) { + maxRows = maxRows + 1; + } + + sb.append(" ").append(NEW_LINE).append(LIMIT).append(" "); + if (maxRows > 0) { + sb.append(maxRows); + } + if (firstRow > 0) { + sb.append(" ").append(OFFSET).append(" "); + sb.append(firstRow); + } + + String sql = request.getDbPlatform().completeSql(sb.toString(), request.getOrmQuery()); + + return new SqlLimitResponse(sql, false); + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/MsSqlServer2000Platform.java b/src/main/java/com/avaje/ebean/config/dbplatform/MsSqlServer2000Platform.java new file mode 100644 index 000000000..26c8923a8 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/MsSqlServer2000Platform.java @@ -0,0 +1,48 @@ +package com.avaje.ebean.config.dbplatform; + +import java.sql.Types; + +/** + * Microsoft SQL Server 2000 specific platform. + *

    + *

      + *
    • supportsGetGeneratedKeys = false
    • + *
    • Use select @@IDENTITY to return the generated Id instead
    • + *
    • Uses LIMIT OFFSET clause
    • + *
    • Uses [ & ] for quoted identifiers
    • + *
    + *

    + */ +public class MsSqlServer2000Platform extends DatabasePlatform { + + public MsSqlServer2000Platform() { + super(); + this.name = "mssqlserver2000"; + this.dbIdentity.setIdType(IdType.IDENTITY); + this.dbIdentity.setSupportsGetGeneratedKeys(false); + this.dbIdentity.setSelectLastInsertedIdTemplate("select @@IDENTITY as X"); + this.dbIdentity.setSupportsIdentity(true); + + this.openQuote = "["; + this.closeQuote = "]"; + + dbTypeMap.put(Types.BOOLEAN, new DbType("bit default 0")); + + dbTypeMap.put(Types.BIGINT, new DbType("numeric", 19)); + dbTypeMap.put(Types.REAL, new DbType("float(16)")); + dbTypeMap.put(Types.DOUBLE, new DbType("float(32)")); + dbTypeMap.put(Types.TINYINT, new DbType("smallint")); + dbTypeMap.put(Types.DECIMAL, new DbType("numeric", 28)); + + dbTypeMap.put(Types.BLOB, new DbType("image")); + dbTypeMap.put(Types.CLOB, new DbType("text")); + dbTypeMap.put(Types.LONGVARBINARY, new DbType("image")); + dbTypeMap.put(Types.LONGVARCHAR, new DbType("text")); + + dbTypeMap.put(Types.DATE, new DbType("datetime")); + dbTypeMap.put(Types.TIME, new DbType("datetime")); + dbTypeMap.put(Types.TIMESTAMP, new DbType("datetime")); + + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/MsSqlServer2005Platform.java b/src/main/java/com/avaje/ebean/config/dbplatform/MsSqlServer2005Platform.java new file mode 100644 index 000000000..65149df87 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/MsSqlServer2005Platform.java @@ -0,0 +1,48 @@ +package com.avaje.ebean.config.dbplatform; + +import java.sql.Types; + +/** + * Microsoft SQL Server 2005 specific platform. + *

    + *

      + *
    • supportsGetGeneratedKeys = true
    • + *
    • Uses LIMIT OFFSET clause
    • + *
    • Uses [ & ] for quoted identifiers
    • + *
    + *

    + */ +public class MsSqlServer2005Platform extends DatabasePlatform { + + public MsSqlServer2005Platform() { + super(); + this.name = "mssqlserver2005"; + this.sqlLimiter = new MsSqlServer2005SqlLimiter(); + this.dbDdlSyntax.setIdentity("identity(1,1)"); + this.dbIdentity.setIdType(IdType.IDENTITY); + this.dbIdentity.setSupportsGetGeneratedKeys(true); + this.dbIdentity.setSupportsIdentity(true); + + this.openQuote = "["; + this.closeQuote = "]"; + + dbTypeMap.put(Types.BOOLEAN, new DbType("bit default 0")); + + dbTypeMap.put(Types.BIGINT, new DbType("numeric", 19)); + dbTypeMap.put(Types.REAL, new DbType("float(16)")); + dbTypeMap.put(Types.DOUBLE, new DbType("float(32)")); + dbTypeMap.put(Types.TINYINT, new DbType("smallint")); + dbTypeMap.put(Types.DECIMAL, new DbType("numeric", 28)); + + dbTypeMap.put(Types.BLOB, new DbType("image")); + dbTypeMap.put(Types.CLOB, new DbType("text")); + dbTypeMap.put(Types.LONGVARBINARY, new DbType("image")); + dbTypeMap.put(Types.LONGVARCHAR, new DbType("text")); + + dbTypeMap.put(Types.DATE, new DbType("datetime")); + dbTypeMap.put(Types.TIME, new DbType("datetime")); + dbTypeMap.put(Types.TIMESTAMP, new DbType("datetime")); + + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/MsSqlServer2005SqlLimiter.java b/src/main/java/com/avaje/ebean/config/dbplatform/MsSqlServer2005SqlLimiter.java new file mode 100644 index 000000000..0e25425ca --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/MsSqlServer2005SqlLimiter.java @@ -0,0 +1,79 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Use top and row_number() function to limit sql results. + */ +public class MsSqlServer2005SqlLimiter implements SqlLimiter { + + final String rowNumberWindowAlias; + + /** + * Specify the name of the rowNumberWindowAlias. + */ + public MsSqlServer2005SqlLimiter(String rowNumberWindowAlias) { + this.rowNumberWindowAlias = rowNumberWindowAlias; + } + + public MsSqlServer2005SqlLimiter() { + this("as limitresult"); + } + + public SqlLimitResponse limit(SqlLimitRequest request) { + + StringBuilder sb = new StringBuilder(500); + + int firstRow = request.getFirstRow(); + + int lastRow = request.getMaxRows(); + if (lastRow > 0) { + // fetch 1 more than we return so that + // we know if more rows are available + lastRow = lastRow + firstRow + 1; + } + + if (firstRow < 1) { + // just use top n + sb.append(" select top ").append(lastRow).append(" "); + if (request.isDistinct()) { + sb.append("distinct "); + } + sb.append(request.getDbSql()); + return new SqlLimitResponse(sb.toString(), false); + } + + /* + * SELECT * FROM (SELECT TOP 20 ROW_NUMBER() OVER (ORDER BY ...) AS rn, ...) + * AS limitresult WHERE rn >= 11 AND rn <= 20 + */ + + sb.append("select * ").append(NEW_LINE).append("from ( "); + + sb.append("select "); + if (request.isDistinct()) { + sb.append("distinct "); + } + sb.append("top ").append(lastRow); + sb.append(" row_number() over (order by "); + sb.append(request.getDbOrderBy()); + sb.append(") as rn, "); + sb.append(request.getDbSql()); + + sb.append(NEW_LINE).append(") "); + sb.append(rowNumberWindowAlias); + sb.append(" where "); + if (firstRow > 0) { + sb.append(" rn > ").append(firstRow); + if (lastRow > 0) { + sb.append(" and "); + } + } + if (lastRow > 0) { + sb.append(" rn <= ").append(lastRow); + } + + String sql = request.getDbPlatform().completeSql(sb.toString(), request.getOrmQuery()); + + return new SqlLimitResponse(sql, true); + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/MySqlBlob.java b/src/main/java/com/avaje/ebean/config/dbplatform/MySqlBlob.java new file mode 100644 index 000000000..b73a734ff --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/MySqlBlob.java @@ -0,0 +1,35 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Support for blob, mediumblob or longblob selection based on the deployment + * length. + *

    + * If no deployment length is defined longblob is used. + *

    + */ +public class MySqlBlob extends DbType { + + private static final int POWER_2_16 = 65536; + private static final int POWER_2_24 = 16777216; + + public MySqlBlob() { + super("blob"); + } + + @Override + public String renderType(int deployLength, int deployScale) { + + if (deployLength >= POWER_2_24) { + return "longblob"; + } + if (deployLength >= POWER_2_16) { + return "mediumblob"; + } + if (deployLength < 1) { + // length not explicitly defined + return "longblob"; + } + return "blob"; + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/MySqlClob.java b/src/main/java/com/avaje/ebean/config/dbplatform/MySqlClob.java new file mode 100644 index 000000000..512a48ac8 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/MySqlClob.java @@ -0,0 +1,35 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Support for text, mediumtext or longtext selection based on the deployment + * length. + *

    + * If no deployment length is defined longtext is used. + *

    + */ +public class MySqlClob extends DbType { + + private static final int POWER_2_16 = 65536; + private static final int POWER_2_24 = 16777216; + + public MySqlClob() { + super("text"); + } + + @Override + public String renderType(int deployLength, int deployScale) { + + if (deployLength >= POWER_2_24) { + return "longtext"; + } + if (deployLength >= POWER_2_16) { + return "mediumtext"; + } + if (deployLength < 1) { + // length not explicitly defined + return "longtext"; + } + return "text"; + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/MySqlDbEncrypt.java b/src/main/java/com/avaje/ebean/config/dbplatform/MySqlDbEncrypt.java new file mode 100644 index 000000000..864cf9fa1 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/MySqlDbEncrypt.java @@ -0,0 +1,36 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * MySql aes_encrypt aes_decrypt based encryption support. + * + * @author rbygrave + */ +public class MySqlDbEncrypt extends AbstractDbEncrypt { + + public MySqlDbEncrypt() { + this.varcharEncryptFunction = new MyVarcharFunction(); + this.dateEncryptFunction = new MyDateFunction(); + } + + private static class MyVarcharFunction implements DbEncryptFunction { + + public String getDecryptSql(String columnWithTableAlias) { + return "CONVERT(AES_DECRYPT(" + columnWithTableAlias + ",?) USING UTF8)"; + } + + public String getEncryptBindSql() { + return "AES_ENCRYPT(?,?)"; + } + } + + private static class MyDateFunction implements DbEncryptFunction { + + public String getDecryptSql(String columnWithTableAlias) { + return "STR_TO_DATE(AES_DECRYPT(" + columnWithTableAlias + ",?),'%Y%d%m')"; + } + + public String getEncryptBindSql() { + return "AES_ENCRYPT(DATE_FORMAT(?,'%Y%d%m'),?)"; + } + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/MySqlPlatform.java b/src/main/java/com/avaje/ebean/config/dbplatform/MySqlPlatform.java new file mode 100644 index 000000000..fba60277e --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/MySqlPlatform.java @@ -0,0 +1,65 @@ +package com.avaje.ebean.config.dbplatform; + +import com.avaje.ebean.BackgroundExecutor; + +import javax.sql.DataSource; +import java.sql.Types; + +/** + * MySQL specific platform. + *

    + *

      + *
    • supportsGetGeneratedKeys = true
    • + *
    • Uses LIMIT OFFSET clause
    • + *
    • Uses ` for quoted identifiers
    • + *
    + *

    + */ +public class MySqlPlatform extends DatabasePlatform { + + public MySqlPlatform() { + super(); + this.name = "mysql"; + this.selectCountWithAlias = true; + this.dbEncrypt = new MySqlDbEncrypt(); + + this.dbIdentity.setIdType(IdType.IDENTITY); + this.dbIdentity.setSupportsGetGeneratedKeys(true); + this.dbIdentity.setSupportsIdentity(true); + this.dbIdentity.setSupportsSequence(false); + + this.openQuote = "`"; + this.closeQuote = "`"; + + this.booleanDbType = Types.BIT; + + dbTypeMap.put(Types.BIT, new DbType("tinyint(1) default 0")); + dbTypeMap.put(Types.BOOLEAN, new DbType("tinyint(1) default 0")); + dbTypeMap.put(Types.TIMESTAMP, new DbType("datetime")); + dbTypeMap.put(Types.CLOB, new MySqlClob()); + dbTypeMap.put(Types.BLOB, new MySqlBlob()); + dbTypeMap.put(Types.BINARY, new DbType("binary", 255)); + dbTypeMap.put(Types.VARBINARY, new DbType("varbinary", 255)); + + dbDdlSyntax.setMaxConstraintNameLength(64); + dbDdlSyntax.setDisableReferentialIntegrity("SET FOREIGN_KEY_CHECKS=0"); + dbDdlSyntax.setEnableReferentialIntegrity("SET FOREIGN_KEY_CHECKS=1"); + dbDdlSyntax.setForeignKeySuffix("on delete restrict on update restrict"); + + } + + /** + * Return null in case there is a sequence annotation. + */ + @Override + public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, + DataSource ds, String seqName, int batchSize) { + + return null; + } + + @Override + protected String withForUpdate(String sql) { + return sql + " for update"; + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/Oracle10DbEncrypt.java b/src/main/java/com/avaje/ebean/config/dbplatform/Oracle10DbEncrypt.java new file mode 100644 index 000000000..b9a4e7b60 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/Oracle10DbEncrypt.java @@ -0,0 +1,107 @@ +package com.avaje.ebean.config.dbplatform; + +import com.avaje.ebean.config.GlobalProperties; + +/** + * Oracle encryption support. + * + *

    + * You will typically need to create your own encryption and decryption + * functions similar to the example ones below. + *

    + * + *
    + * 
    + *  // Remember your DB user needs execute privilege on DBMS_CRYPTO 
    + *  // as well as your encryption and decryption functions
    + *  
    + *  
    + *  // This is an Example Encryption function only - please create your own.
    + * 
    + * CREATE OR REPLACE FUNCTION eb_encrypt(data IN VARCHAR, key in VARCHAR) RETURN RAW IS
    + * 
    + *     encryption_mode NUMBER := DBMS_CRYPTO.ENCRYPT_AES128 + DBMS_CRYPTO.CHAIN_CBC  + DBMS_CRYPTO.PAD_PKCS5;
    + * 
    + *     BEGIN
    + *          RETURN DBMS_CRYPTO.ENCRYPT(UTL_I18N.STRING_TO_RAW (data, 'AL32UTF8'), 
    + *            encryption_mode, UTL_I18N.STRING_TO_RAW(key, 'AL32UTF8') );
    + *     END;
    + *     /
    + *     
    + *     
    + *     
    + *  // This is an Example Decryption function only - please create your own.
    + *     
    + * CREATE OR REPLACE FUNCTION eb_decrypt(data IN RAW, key IN VARCHAR) RETURN VARCHAR IS
    + * 
    + *     encryption_mode NUMBER := DBMS_CRYPTO.ENCRYPT_AES128 + DBMS_CRYPTO.CHAIN_CBC  + DBMS_CRYPTO.PAD_PKCS5;
    + * 
    + *     BEGIN
    + *          RETURN UTL_RAW.CAST_TO_VARCHAR2(DBMS_CRYPTO.DECRYPT
    + *            (data, encryption_mode, UTL_I18N.STRING_TO_RAW(key, 'AL32UTF8')));
    + *     END;
    + *     /
    + * 
    + * + * @author rbygrave + */ +public class Oracle10DbEncrypt extends AbstractDbEncrypt { + + /** + * Constructs the Oracle10DbEncrypt. + */ + public Oracle10DbEncrypt() { + + String encryptfunction = GlobalProperties.get("ebean.oracle.encryptfunction", "eb_encrypt"); + String decryptfunction = GlobalProperties.get("ebean.oracle.decryptfunction", "eb_decrypt"); + + this.varcharEncryptFunction = new OraVarcharFunction(encryptfunction, decryptfunction); + this.dateEncryptFunction = new OraDateFunction(encryptfunction, decryptfunction); + } + + /** + * VARCHAR encryption/decryption function. + */ + private static class OraVarcharFunction implements DbEncryptFunction { + + private final String encryptfunction; + private final String decryptfunction; + + public OraVarcharFunction(String encryptfunction, String decryptfunction) { + this.encryptfunction = encryptfunction; + this.decryptfunction = decryptfunction; + } + + public String getDecryptSql(String columnWithTableAlias) { + return decryptfunction + "(" + columnWithTableAlias + ",?)"; + } + + public String getEncryptBindSql() { + return encryptfunction + "(?,?)"; + } + + } + + /** + * DATE encryption/decryption function. + */ + private static class OraDateFunction implements DbEncryptFunction { + + private final String encryptfunction; + private final String decryptfunction; + + public OraDateFunction(String encryptfunction, String decryptfunction) { + this.encryptfunction = encryptfunction; + this.decryptfunction = decryptfunction; + } + + public String getDecryptSql(String columnWithTableAlias) { + return "to_date(" + decryptfunction + "(" + columnWithTableAlias + ",?),'YYYYMMDD')"; + } + + public String getEncryptBindSql() { + return encryptfunction + "(to_char(?,'YYYYMMDD'),?)"; + } + + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/Oracle10Platform.java b/src/main/java/com/avaje/ebean/config/dbplatform/Oracle10Platform.java new file mode 100644 index 000000000..796e69327 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/Oracle10Platform.java @@ -0,0 +1,67 @@ +package com.avaje.ebean.config.dbplatform; + +import com.avaje.ebean.BackgroundExecutor; + +import javax.sql.DataSource; +import java.sql.Types; + +/** + * Oracle10 and greater specific platform. + */ +public class Oracle10Platform extends DatabasePlatform { + + public Oracle10Platform() { + super(); + this.name = "oracle"; + this.dbEncrypt = new Oracle10DbEncrypt(); + + this.sqlLimiter = new RownumSqlLimiter(); + + // Not using getGeneratedKeys as instead we will + // batch load sequences which enables JDBC batch execution + dbIdentity.setSupportsGetGeneratedKeys(false); + dbIdentity.setIdType(IdType.SEQUENCE); + dbIdentity.setSupportsSequence(true); + + this.treatEmptyStringsAsNull = true; + + this.openQuote = "\""; + this.closeQuote = "\""; + + booleanDbType = Types.INTEGER; + dbTypeMap.put(Types.BOOLEAN, new DbType("number(1) default 0")); + + dbTypeMap.put(Types.INTEGER, new DbType("number", 10)); + dbTypeMap.put(Types.BIGINT, new DbType("number", 19)); + dbTypeMap.put(Types.REAL, new DbType("number", 19, 4)); + dbTypeMap.put(Types.DOUBLE, new DbType("number", 19, 4)); + dbTypeMap.put(Types.SMALLINT, new DbType("number", 5)); + dbTypeMap.put(Types.TINYINT, new DbType("number", 3)); + dbTypeMap.put(Types.DECIMAL, new DbType("number", 38)); + + dbTypeMap.put(Types.VARCHAR, new DbType("varchar2", 255)); + + dbTypeMap.put(Types.LONGVARBINARY, new DbType("blob")); + dbTypeMap.put(Types.LONGVARCHAR, new DbType("clob")); + dbTypeMap.put(Types.VARBINARY, new DbType("raw", 255)); + dbTypeMap.put(Types.BINARY, new DbType("raw", 255)); + + dbTypeMap.put(Types.TIME, new DbType("timestamp")); + + dbDdlSyntax.setDropTableCascade("cascade constraints purge"); + dbDdlSyntax.setIdentity(null); + dbDdlSyntax.setMaxConstraintNameLength(30); + } + + @Override + public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, + String seqName, int batchSize) { + + return new OracleSequenceIdGenerator(be, ds, seqName, batchSize); + } + + @Override + protected String withForUpdate(String sql) { + return sql + " for update"; + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/Oracle9Platform.java b/src/main/java/com/avaje/ebean/config/dbplatform/Oracle9Platform.java new file mode 100644 index 000000000..7411e1747 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/Oracle9Platform.java @@ -0,0 +1,15 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Since Ebean v2.2.0 Oracle9 is no different from Oracle10. + *

    + * This will be removed in the future. + *

    + */ +public class Oracle9Platform extends Oracle10Platform { + + public Oracle9Platform() { + super(); + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/OracleSequenceIdGenerator.java b/src/main/java/com/avaje/ebean/config/dbplatform/OracleSequenceIdGenerator.java new file mode 100644 index 000000000..52436bb30 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/OracleSequenceIdGenerator.java @@ -0,0 +1,27 @@ +package com.avaje.ebean.config.dbplatform; + +import javax.sql.DataSource; + +import com.avaje.ebean.BackgroundExecutor; + +/** + * Oracle specific sequence Id Generator. + */ +public class OracleSequenceIdGenerator extends SequenceIdGenerator { + + private final String baseSql; + + /** + * Construct given a dataSource and sql to return the next sequence value. + */ + public OracleSequenceIdGenerator(BackgroundExecutor be, DataSource ds, String seqName, + int batchSize) { + super(be, ds, seqName, batchSize); + this.baseSql = "select " + seqName + + ".nextval, a from (select level as a FROM dual CONNECT BY level <= "; + } + + public String getSql(int batchSize) { + return baseSql + batchSize + ")"; + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/PostgresDbEncrypt.java b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresDbEncrypt.java new file mode 100644 index 000000000..65516b79b --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresDbEncrypt.java @@ -0,0 +1,36 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Postgres pgp_sym_encrypt pgp_sym_decrypt based encryption support. + * + * @author rbygrave + */ +public class PostgresDbEncrypt extends AbstractDbEncrypt { + + public PostgresDbEncrypt() { + this.varcharEncryptFunction = new PgVarcharFunction(); + this.dateEncryptFunction = new PgDateFunction(); + } + + private static class PgVarcharFunction implements DbEncryptFunction { + + public String getDecryptSql(String columnWithTableAlias) { + return "pgp_sym_decrypt(" + columnWithTableAlias + ",?)"; + } + + public String getEncryptBindSql() { + return "pgp_sym_encrypt(?,?)"; + } + } + + private static class PgDateFunction implements DbEncryptFunction { + + public String getDecryptSql(String columnWithTableAlias) { + return "to_date(pgp_sym_decrypt(" + columnWithTableAlias + ",?),'YYYYMMDD')"; + } + + public String getEncryptBindSql() { + return "pgp_sym_encrypt(to_char(?::date,'YYYYMMDD'),?)"; + } + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java new file mode 100644 index 000000000..e2158ce97 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java @@ -0,0 +1,73 @@ +package com.avaje.ebean.config.dbplatform; + +import com.avaje.ebean.BackgroundExecutor; +import com.avaje.ebean.config.GlobalProperties; + +import javax.sql.DataSource; +import java.sql.Types; + +/** + * Postgres v8.3 specific platform. + *

    + * No support for getGeneratedKeys. + *

    + */ +public class PostgresPlatform extends DatabasePlatform { + + public PostgresPlatform() { + super(); + this.name = "postgres"; + this.selectCountWithAlias = true; + this.blobDbType = Types.LONGVARBINARY; + this.clobDbType = Types.VARCHAR; + + this.dbEncrypt = new PostgresDbEncrypt(); + + this.dbIdentity.setSupportsGetGeneratedKeys(false); + this.dbIdentity.setIdType(IdType.SEQUENCE); + this.dbIdentity.setSupportsSequence(true); + + String colAlias = GlobalProperties.get("ebean.columnAliasPrefix", null); + if (colAlias == null) { + // Postgres requires the "as" keyword for column alias + GlobalProperties.put("ebean.columnAliasPrefix", "as c"); + } + + this.openQuote = "\""; + this.closeQuote = "\""; + + // dbTypeMap.put(Types.BOOLEAN, new DbType("bit default 0")); + + dbTypeMap.put(Types.INTEGER, new DbType("integer", false)); + dbTypeMap.put(Types.DOUBLE, new DbType("float")); + dbTypeMap.put(Types.TINYINT, new DbType("smallint")); + dbTypeMap.put(Types.DECIMAL, new DbType("decimal", 38)); + + dbTypeMap.put(Types.BINARY, new DbType("bytea", false)); + dbTypeMap.put(Types.VARBINARY, new DbType("bytea", false)); + + dbTypeMap.put(Types.BLOB, new DbType("bytea", false)); + dbTypeMap.put(Types.CLOB, new DbType("text")); + dbTypeMap.put(Types.LONGVARBINARY, new DbType("bytea", false)); + dbTypeMap.put(Types.LONGVARCHAR, new DbType("text")); + + dbDdlSyntax.setDropTableCascade("cascade"); + dbDdlSyntax.setDropIfExists("if exists"); + + } + + /** + * Create a Postgres specific sequence IdGenerator. + */ + @Override + public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, + String seqName, int batchSize) { + + return new PostgresSequenceIdGenerator(be, ds, seqName, batchSize); + } + + @Override + protected String withForUpdate(String sql) { + return sql + " for update"; + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/PostgresSequenceIdGenerator.java b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresSequenceIdGenerator.java new file mode 100644 index 000000000..eb7af2632 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresSequenceIdGenerator.java @@ -0,0 +1,27 @@ +package com.avaje.ebean.config.dbplatform; + +import javax.sql.DataSource; + +import com.avaje.ebean.BackgroundExecutor; + +/** + * Postgres specific sequence Id Generator. + */ +public class PostgresSequenceIdGenerator extends SequenceIdGenerator { + + private final String baseSql; + + /** + * Construct given a dataSource and sql to return the next sequence value. + */ + public PostgresSequenceIdGenerator(BackgroundExecutor be, DataSource ds, String seqName, + int batchSize) { + super(be, ds, seqName, batchSize); + this.baseSql = "select nextval('" + seqName + "'), s.generate_series from (" + + "select generate_series from generate_series(1,"; + } + + public String getSql(int batchSize) { + return baseSql + batchSize + ") ) as s"; + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/RowNumberSqlLimiter.java b/src/main/java/com/avaje/ebean/config/dbplatform/RowNumberSqlLimiter.java new file mode 100644 index 000000000..15571be28 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/RowNumberSqlLimiter.java @@ -0,0 +1,72 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Adds the ROW_NUMBER() OVER function to a query. + */ +public class RowNumberSqlLimiter implements SqlLimiter { + + /** + * ROW_NUMBER() OVER (ORDER BY + */ + private static final String ROW_NUMBER_OVER = "row_number() over (order by "; + + /** + * ) as rn, + */ + private static final String ROW_NUMBER_AS = ") as rn, "; + + final String rowNumberWindowAlias; + + /** + * Specify the name of the rowNumberWindowAlias. + */ + public RowNumberSqlLimiter(String rowNumberWindowAlias) { + this.rowNumberWindowAlias = rowNumberWindowAlias; + } + + public RowNumberSqlLimiter() { + this("as limitresult"); + } + + public SqlLimitResponse limit(SqlLimitRequest request) { + + StringBuilder sb = new StringBuilder(500); + + int firstRow = request.getFirstRow(); + + int lastRow = request.getMaxRows(); + if (lastRow > 0) { + lastRow = lastRow + firstRow + 1; + } + + sb.append("select * from (").append(NEW_LINE); + + sb.append("select "); + if (request.isDistinct()) { + sb.append("distinct "); + } + + sb.append(ROW_NUMBER_OVER); + sb.append(request.getDbOrderBy()); + sb.append(ROW_NUMBER_AS); + + sb.append(request.getDbSql()); + + sb.append(NEW_LINE).append(") "); + sb.append(rowNumberWindowAlias); + sb.append(" where "); + if (firstRow > 0) { + sb.append(" rn > ").append(firstRow); + if (lastRow > 0) { + sb.append(" and "); + } + } + if (lastRow > 0) { + sb.append(" rn <= ").append(lastRow); + } + + String sql = request.getDbPlatform().completeSql(sb.toString(), request.getOrmQuery()); + + return new SqlLimitResponse(sql, true); + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/RownumSqlLimiter.java b/src/main/java/com/avaje/ebean/config/dbplatform/RownumSqlLimiter.java new file mode 100644 index 000000000..d62e392ca --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/RownumSqlLimiter.java @@ -0,0 +1,78 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Add ROWNUM column etc around SQL query to limit results. + */ +public class RownumSqlLimiter implements SqlLimiter { + + private final String rnum; + + private final boolean useFirstRowsHint; + + /** + * Create with default inner rownum column alias and used FIRST_ROWS hint. + */ + public RownumSqlLimiter() { + this("rn_", true); + } + + /** + * Specify the inner rownum column alias and whether to include the FIRST_ROWS + * hint. + */ + public RownumSqlLimiter(String rnum, boolean useFirstRowsHint) { + this.rnum = rnum; + this.useFirstRowsHint = useFirstRowsHint; + } + + public SqlLimitResponse limit(SqlLimitRequest request) { + + // select * + // from ( select /*+ FIRST_ROWS(n) */ ROWNUM rnum, a.* + // from ( your_query_goes_here, + // with order by ) a + // where ROWNUM <= + // :MAX_ROW_TO_FETCH ) + // where rnum >= :MIN_ROW_TO_FETCH; + + StringBuilder sb = new StringBuilder(500); + + int firstRow = request.getFirstRow(); + + int lastRow = request.getMaxRows(); + if (lastRow > 0) { + lastRow = lastRow + firstRow + 1; + } + + sb.append("select * ").append(NEW_LINE).append("from ( "); + + sb.append("select "); + if (useFirstRowsHint && request.getMaxRows() > 0) { + sb.append("/*+ FIRST_ROWS(").append(request.getMaxRows() + 1).append(") */ "); + } + + sb.append("rownum ").append(rnum).append(", a.* ").append(NEW_LINE); + sb.append(" from (");// + + sb.append(" select "); + if (request.isDistinct()) { + sb.append("distinct "); + } + sb.append(request.getDbSql()); + + sb.append(NEW_LINE).append(" ) a "); + if (lastRow > 0) { + sb.append(NEW_LINE).append(" where rownum <= ").append(lastRow); + } + sb.append(NEW_LINE).append(" ) "); + if (firstRow > 0) { + sb.append(NEW_LINE).append("where "); + sb.append(rnum).append(" > ").append(firstRow); + } + + String sql = request.getDbPlatform().completeSql(sb.toString(), request.getOrmQuery()); + + return new SqlLimitResponse(sql, true); + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/SQLitePlatform.java b/src/main/java/com/avaje/ebean/config/dbplatform/SQLitePlatform.java new file mode 100644 index 000000000..6e83b8555 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/SQLitePlatform.java @@ -0,0 +1,42 @@ +package com.avaje.ebean.config.dbplatform; + +import java.sql.Types; + +import javax.sql.DataSource; + +import com.avaje.ebean.BackgroundExecutor; + +public class SQLitePlatform extends DatabasePlatform { + + public SQLitePlatform() { + super(); + this.name = "sqlite"; + + this.dbIdentity.setIdType(IdType.IDENTITY); + this.dbIdentity.setSupportsGetGeneratedKeys(false); + this.dbIdentity.setSelectLastInsertedIdTemplate("select last_insert_rowid()"); + this.openQuote = "\""; + this.closeQuote = "\""; + + this.booleanDbType = Types.INTEGER; + + dbTypeMap.put(Types.BIT, new DbType("int default 0")); + dbTypeMap.put(Types.BOOLEAN, new DbType("int default 0")); + + dbDdlSyntax.setInlinePrimaryKeyConstraint(true); + dbDdlSyntax.setIdentity("AUTOINCREMENT"); + dbDdlSyntax.setDisableReferentialIntegrity("PRAGMA foreign_keys = OFF"); + dbDdlSyntax.setEnableReferentialIntegrity("PRAGMA foreign_keys = ON"); + } + + /** + * Return null in case there is a sequence annotation. + */ + @Override + public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, + DataSource ds, String seqName, int batchSize) { + + return null; + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/SequenceIdGenerator.java b/src/main/java/com/avaje/ebean/config/dbplatform/SequenceIdGenerator.java new file mode 100644 index 000000000..63beef4f6 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/SequenceIdGenerator.java @@ -0,0 +1,249 @@ +package com.avaje.ebean.config.dbplatform; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; + +import javax.persistence.PersistenceException; +import javax.sql.DataSource; + +import com.avaje.ebean.BackgroundExecutor; +import com.avaje.ebean.Transaction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Database sequence based IdGenerator. + */ +public abstract class SequenceIdGenerator implements IdGenerator { + + private static final Logger logger = LoggerFactory.getLogger(SequenceIdGenerator.class); + + /** + * Used to synchronise the idList access. + */ + protected final Object monitor = new Object(); + + /** + * Used to synchronise background loading (loadBatchInBackground). + */ + protected final Object backgroundLoadMonitor = new Object(); + + /** + * The actual sequence name. + */ + protected final String seqName; + + protected final DataSource dataSource; + + protected final BackgroundExecutor backgroundExecutor; + + protected final ArrayList idList = new ArrayList(50); + + protected int batchSize; + + protected int currentlyBackgroundLoading; + + /** + * Construct given a dataSource and sql to return the next sequence value. + */ + public SequenceIdGenerator(BackgroundExecutor be, DataSource ds, String seqName, int batchSize) { + this.backgroundExecutor = be; + this.dataSource = ds; + this.seqName = seqName; + this.batchSize = batchSize; + } + + public abstract String getSql(int batchSize); + + /** + * Returns the sequence name. + */ + public String getName() { + return seqName; + } + + /** + * Returns true. + */ + public boolean isDbSequence() { + return true; + } + + /** + * If allocateSize is large load some sequences in a background thread. + *

    + * For example, when inserting a bean with a cascade on a OneToMany with many + * beans Ebean can call this to ensure . + *

    + */ + public void preAllocateIds(int allocateSize) { + if (batchSize > 1 && allocateSize > batchSize) { + // only bother if allocateSize is bigger than + // the normal loading batchSize + if (allocateSize > 100) { + // max out at 100 for now + allocateSize = 100; + } + loadLargeAllocation(allocateSize); + } + } + + /** + * Called by preAllocateIds when we know that a large number of Id's is going + * to be needed shortly. + */ + protected void loadLargeAllocation(final int allocateSize) { + // preAllocateIds was called with a relatively large batchSize + // so we will just go ahead and load those anyway in background + backgroundExecutor.execute(new Runnable() { + public void run() { + loadMoreIds(allocateSize, null); + } + }); + } + + /** + * Return the next Id. + *

    + * If a Transaction has been passed in use the Connection from it. + *

    + */ + public Object nextId(Transaction t) { + synchronized (monitor) { + + if (idList.size() == 0) { + loadMoreIds(batchSize, t); + } + Integer nextId = idList.remove(0); + + if (batchSize > 1) { + if (idList.size() <= batchSize / 2) { + loadBatchInBackground(); + } + } + + return nextId; + } + } + + /** + * Load another batch of Id's using a background thread. + */ + protected void loadBatchInBackground() { + + // single threaded processing... + synchronized (backgroundLoadMonitor) { + + if (currentlyBackgroundLoading > 0) { + // skip as already background loading + logger.debug("... skip background sequence load (another load in progress)"); + return; + } + + currentlyBackgroundLoading = batchSize; + + backgroundExecutor.execute(new Runnable() { + public void run() { + loadMoreIds(batchSize, null); + synchronized (backgroundLoadMonitor) { + currentlyBackgroundLoading = 0; + } + } + }); + } + } + + protected void loadMoreIds(final int numberToLoad, Transaction t) { + + ArrayList newIds = getMoreIds(numberToLoad, t); + + if (logger.isDebugEnabled()) { + logger.debug("... seq:" + seqName + " loaded:" + numberToLoad + " ids:" + newIds); + } + + synchronized (monitor) { + for (int i = 0; i < newIds.size(); i++) { + idList.add(newIds.get(i)); + } + } + } + + /** + * Get more Id's by executing a query and reading the Id's returned. + */ + protected ArrayList getMoreIds(int loadSize, Transaction t) { + + String sql = getSql(loadSize); + + ArrayList newIds = new ArrayList(loadSize); + + boolean useTxnConnection = t != null; + + Connection c = null; + PreparedStatement pstmt = null; + ResultSet rset = null; + try { + c = useTxnConnection ? t.getConnection() : dataSource.getConnection(); + + pstmt = c.prepareStatement(sql); + rset = pstmt.executeQuery(); + while (rset.next()) { + int val = rset.getInt(1); + newIds.add(Integer.valueOf(val)); + } + if (newIds.size() == 0) { + String m = "Always expecting more than 1 row from " + sql; + throw new PersistenceException(m); + } + + return newIds; + + } catch (SQLException e) { + if (e.getMessage().contains("Database is already closed")) { + String msg = "Error getting SEQ when DB shutting down " + e.getMessage(); + logger.info(msg); + System.out.println(msg); + return newIds; + } else { + throw new PersistenceException("Error getting sequence nextval", e); + } + } finally { + if (useTxnConnection) { + closeResources(null, pstmt, rset); + } else { + closeResources(c, pstmt, rset); + } + } + } + + /** + * Close the JDBC resources. + */ + protected void closeResources(Connection c, PreparedStatement pstmt, ResultSet rset) { + try { + if (rset != null) { + rset.close(); + } + } catch (SQLException e) { + logger.error( "Error closing ResultSet", e); + } + try { + if (pstmt != null) { + pstmt.close(); + } + } catch (SQLException e) { + logger.error("Error closing PreparedStatement", e); + } + try { + if (c != null) { + c.close(); + } + } catch (SQLException e) { + logger.error("Error closing Connection", e); + } + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/SimpleSequenceIdGenerator.java b/src/main/java/com/avaje/ebean/config/dbplatform/SimpleSequenceIdGenerator.java new file mode 100644 index 000000000..2574e418b --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/SimpleSequenceIdGenerator.java @@ -0,0 +1,106 @@ +package com.avaje.ebean.config.dbplatform; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import javax.persistence.PersistenceException; +import javax.sql.DataSource; + +import com.avaje.ebean.Transaction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A very simple Database sequence based IdGenerator. + *

    + * One which batch requests sequence Id's would be better for performance. + *

    + */ +public class SimpleSequenceIdGenerator implements IdGenerator { + + private static final Logger logger = LoggerFactory.getLogger(SimpleSequenceIdGenerator.class); + + private final String sql; + + private final DataSource dataSource; + + private final String seqName; + + /** + * Construct given a dataSource and sql to return the next sequence value. + */ + public SimpleSequenceIdGenerator(DataSource dataSource, String sql, String seqName) { + this.dataSource = dataSource; + this.sql = sql; + this.seqName = seqName; + } + + public String getName() { + return seqName; + } + + public boolean isDbSequence() { + return true; + } + + public void preAllocateIds(int batchSize) { + // just ignore this + } + + public Object nextId(Transaction t) { + + boolean useTxnConnection = t != null; + + Connection c = null; + PreparedStatement pstmt = null; + ResultSet rset = null; + try { + c = useTxnConnection ? t.getConnection() : dataSource.getConnection(); + pstmt = c.prepareStatement(sql); + rset = pstmt.executeQuery(); + if (rset.next()) { + int val = rset.getInt(1); + return Integer.valueOf(val); + } else { + String m = "Always expecting 1 row from " + sql; + throw new PersistenceException(m); + } + } catch (SQLException e) { + throw new PersistenceException("Error getting sequence nextval", e); + + } finally { + if (useTxnConnection) { + closeResources(rset, pstmt, null); + } else { + closeResources(rset, pstmt, c); + } + } + } + + private void closeResources(ResultSet rset, PreparedStatement pstmt, Connection c) { + try { + if (rset != null) { + rset.close(); + } + } catch (SQLException e) { + logger.error("Error closing ResultSet", e); + } + try { + if (pstmt != null) { + pstmt.close(); + } + } catch (SQLException e) { + logger.error("Error closing PreparedStatement", e); + } + try { + if (c != null) { + c.close(); + } + } catch (SQLException e) { + logger.error("Error closing Connection", e); + } + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/SqlAnywhereLimiter.java b/src/main/java/com/avaje/ebean/config/dbplatform/SqlAnywhereLimiter.java new file mode 100644 index 000000000..fcdd5bc6f --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/SqlAnywhereLimiter.java @@ -0,0 +1,41 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Use top xx and start at xx to limit sql results. Based on + * MsSqlServer2005SqlLimiter and LimitOffsetSqlLimiter + */ +public class SqlAnywhereLimiter implements SqlLimiter { + + public SqlLimitResponse limit(SqlLimitRequest request) { + + StringBuilder sb = new StringBuilder(500); + + int firstRow = request.getFirstRow(); + int maxRows = request.getMaxRows(); + if (maxRows > 0) { + // fetch 1 more than we return so that + // we know if more rows are available + maxRows = maxRows + 1; + } + + /* + * SELECT TOP xx START AT xx ... FROM ... + */ + sb.append("select "); + if (request.isDistinct()) { + sb.append("distinct "); + } + if (maxRows > 0) { + sb.append("top ").append(maxRows).append(" "); + } + if (firstRow > 0) { + sb.append("start at ").append(firstRow + 1).append(" "); + } + sb.append(request.getDbSql()); + + String sql = request.getDbPlatform().completeSql(sb.toString(), request.getOrmQuery()); + + return new SqlLimitResponse(sql, false); + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/SqlAnywherePlatform.java b/src/main/java/com/avaje/ebean/config/dbplatform/SqlAnywherePlatform.java new file mode 100644 index 000000000..dc9b9360f --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/SqlAnywherePlatform.java @@ -0,0 +1,41 @@ +package com.avaje.ebean.config.dbplatform; + +import java.sql.Types; + +/** + * Sybase SQL Anywhere specific platform. + *

    + *

      + *
    • supportsGetGeneratedKeys = false
    • + *
    • Uses TOP START AT clause
    • + *
    + *

    + */ + +public class SqlAnywherePlatform extends DatabasePlatform { + + public SqlAnywherePlatform() { + super(); + this.name = "sqlanywhere"; + this.dbIdentity.setIdType(IdType.IDENTITY); + + this.sqlLimiter = new SqlAnywhereLimiter(); + this.dbIdentity.setSupportsGetGeneratedKeys(false); + this.dbIdentity.setSelectLastInsertedIdTemplate("select @@IDENTITY as X"); + this.dbIdentity.setSupportsIdentity(true); + + dbTypeMap.put(Types.BOOLEAN, new DbType("bit default 0")); + dbTypeMap.put(Types.BIGINT, new DbType("numeric", 19)); + dbTypeMap.put(Types.REAL, new DbType("float(16)")); + dbTypeMap.put(Types.DOUBLE, new DbType("float(32)")); + dbTypeMap.put(Types.TINYINT, new DbType("smallint")); + dbTypeMap.put(Types.DECIMAL, new DbType("numeric", 28)); + + dbTypeMap.put(Types.BLOB, new DbType("binary(4500)")); + dbTypeMap.put(Types.CLOB, new DbType("long varchar")); + dbTypeMap.put(Types.LONGVARBINARY, new DbType("long binary")); + dbTypeMap.put(Types.LONGVARCHAR, new DbType("long varchar")); + + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/SqlLimitRequest.java b/src/main/java/com/avaje/ebean/config/dbplatform/SqlLimitRequest.java new file mode 100644 index 000000000..9a0e55264 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/SqlLimitRequest.java @@ -0,0 +1,47 @@ +package com.avaje.ebean.config.dbplatform; + +import com.avaje.ebean.Query; + +/** + * The request object for the query that can have sql limiting applied to it + * (such as a LIMIT OFFSET clause). + * + * @author rob + */ +public interface SqlLimitRequest { + + /** + * Return true if the query uses distinct. + */ + public boolean isDistinct(); + + /** + * Return the first row value. + */ + public int getFirstRow(); + + /** + * Return the max rows for this query. + */ + public int getMaxRows(); + + /** + * Return the sql query. + */ + public String getDbSql(); + + /** + * Return the orderBy clause of the sql query. + */ + public String getDbOrderBy(); + + /** + * return the query + */ + public Query getOrmQuery(); + + /** + * return the database platform + */ + public DatabasePlatform getDbPlatform(); +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/SqlLimitResponse.java b/src/main/java/com/avaje/ebean/config/dbplatform/SqlLimitResponse.java new file mode 100644 index 000000000..084a5612c --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/SqlLimitResponse.java @@ -0,0 +1,34 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * The resulting SQL from a SqlLimit process. + */ +public class SqlLimitResponse { + + final String sql; + + final boolean includesRowNumberColumn; + + /** + * Create the response. + */ + public SqlLimitResponse(String sql, boolean includesRowNumberColumn) { + this.sql = sql; + this.includesRowNumberColumn = includesRowNumberColumn; + } + + /** + * The final query sql with SQL limit statements added. + */ + public String getSql() { + return sql; + } + + /** + * Returns true if a ROW_NUMBER column is used in the query. + */ + public boolean isIncludesRowNumberColumn() { + return includesRowNumberColumn; + } + +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/SqlLimiter.java b/src/main/java/com/avaje/ebean/config/dbplatform/SqlLimiter.java new file mode 100644 index 000000000..8a14daeb9 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/SqlLimiter.java @@ -0,0 +1,25 @@ +package com.avaje.ebean.config.dbplatform; + +/** + * Adds SQL limiting to a query (such as LIMIT OFFSET). + */ +public interface SqlLimiter { + + /** + * the new line character used. + *

    + * Note that this is removed for logging sql to the transaction log. + *

    + */ + public static final char NEW_LINE = '\n'; + + /** + * The carriage return character. + */ + public static final char CARRIAGE_RETURN = '\r'; + + /** + * Add the SQL limiting statements around the query. + */ + public SqlLimitResponse limit(SqlLimitRequest request); +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/package.html b/src/main/java/com/avaje/ebean/config/dbplatform/package.html new file mode 100644 index 000000000..cf8b5e070 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/dbplatform/package.html @@ -0,0 +1,10 @@ + + + + Database platform specific support + + +Database platform specific support + + + \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/config/package.html b/src/main/java/com/avaje/ebean/config/package.html new file mode 100644 index 000000000..a3be4d069 --- /dev/null +++ b/src/main/java/com/avaje/ebean/config/package.html @@ -0,0 +1,10 @@ + + + + Configuration settings for EbeanServer construction + + +Configuration settings for EbeanServer construction + + + \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/event/BeanFinder.java b/src/main/java/com/avaje/ebean/event/BeanFinder.java new file mode 100644 index 000000000..356461db6 --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/BeanFinder.java @@ -0,0 +1,34 @@ +package com.avaje.ebean.event; + +import com.avaje.ebean.bean.BeanCollection; + +/** + * Used to override the finding implementation for a bean. + *

    + * For beans that are not in a JDBC data source you can implement this handle + * bean finding. For example, read a log file building each entry as a bean and + * returning that. + *

    + *

    + * There are a number of internal BeanFinders in Ebean to return meta data from + * Ebean at runtime such as query execution statistics etc. See the beans in + * com.avaje.ebean.meta and finders in com.avaje.ebean.server.meta. + *

    + */ +public interface BeanFinder { + + /** + * Find a bean using its id or unique predicate. + */ + public T find(BeanQueryRequest request); + + /** + * Return a List, Set or Map for the given find request. + *

    + * Note the returning object is cast to a List Set or Map so you do need to + * get the return type right. + *

    + */ + public BeanCollection findMany(BeanQueryRequest request); + +} diff --git a/src/main/java/com/avaje/ebean/event/BeanPersistAdapter.java b/src/main/java/com/avaje/ebean/event/BeanPersistAdapter.java new file mode 100644 index 000000000..37e5a125e --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/BeanPersistAdapter.java @@ -0,0 +1,75 @@ +package com.avaje.ebean.event; + +import java.util.Set; + +import com.avaje.ebean.config.ServerConfig; + +/** + * A no operation implementation of BeanPersistController. Objects extending + * this need to only override the methods they want to. + *

    + * A BeanPersistAdapter is either found automatically via class path search or + * can be added programmatically via + * {@link ServerConfig#add(BeanPersistController)} or + * {@link ServerConfig#setPersistControllers(java.util.List)}. + *

    + */ +public abstract class BeanPersistAdapter implements BeanPersistController { + + public abstract boolean isRegisterFor(Class cls); + + /** + * Returns 10 - override this to control the order in which + * BeanPersistController's are executed when there is multiple of them + * registered for a given entity type (class). + */ + public int getExecutionOrder() { + return 10; + } + + /** + * Returns true indicating normal processing should continue. + */ + public boolean preDelete(BeanPersistRequest request) { + return true; + } + + /** + * Returns true indicating normal processing should continue. + */ + public boolean preInsert(BeanPersistRequest request) { + return true; + } + + /** + * Returns true indicating normal processing should continue. + */ + public boolean preUpdate(BeanPersistRequest request) { + return true; + } + + /** + * Does nothing by default. + */ + public void postDelete(BeanPersistRequest request) { + } + + /** + * Does nothing by default. + */ + public void postInsert(BeanPersistRequest request) { + } + + /** + * Does nothing by default. + */ + public void postUpdate(BeanPersistRequest request) { + } + + /** + * Does nothing by default. + */ + public void postLoad(Object bean, Set includedProperties) { + } + +} diff --git a/src/main/java/com/avaje/ebean/event/BeanPersistController.java b/src/main/java/com/avaje/ebean/event/BeanPersistController.java new file mode 100644 index 000000000..37b1c06a2 --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/BeanPersistController.java @@ -0,0 +1,115 @@ +package com.avaje.ebean.event; + +import java.util.Set; + +/** + * Used to enhance or override the default bean persistence mechanism. + *

    + * Note that if want to totally change the finding, you need to use a BeanFinder + * rather than using postLoad(). + *

    + *

    + * Note that getTransaction() on the PersistRequest returns the transaction used + * for the insert, update, delete or fetch. To explicitly use this same + * transaction you should use this transaction via methods on EbeanServer. + *

    + * + *
    + * 
    + *        Object extaBeanToSave = ...;
    + *        Transaction t = request.getTransaction();
    + *        EbeanServer server = request.getEbeanServer();
    + *        server.save(extraBeanToSave, t);
    + * 
    + * 
    + * + *

    + * It is worth noting that BeanPersistListener is different in three main ways + * from BeanPersistController postXXX methods. + *

      + *
    • BeanPersistListener only sees successfully committed events. + * BeanController pre and post methods occur before the commit or a rollback and + * will see events that are later rolled back
    • + *
    • BeanPersistListener runs in a background thread and will not effect the + * response time of the actual persist where as BeanController code will
    • + *
    • BeanPersistListener can be notified of events from other servers in a + * cluster.
    • + *
    + *

    + *

    + * A BeanPersistController is either found automatically via class path search + * or can be added programmatically via ServerConfiguration.addEntity(). + *

    + */ +public interface BeanPersistController { + + /** + * When there are multiple BeanPersistController's for a given entity type + * this controls the order in which they are executed. + *

    + * Lowest values are executed first. + *

    + * + * @return an int used to control the order BeanPersistController's are + * executed + */ + public int getExecutionOrder(); + + /** + * Return true if this BeanPersistController should be registered for events + * on this entity type. + */ + public boolean isRegisterFor(Class cls); + + /** + * Prior to the insert perform some action. Return true if you want the + * default functionality to continue. + *

    + * Return false if you have completely replaced the insert functionality and + * do not want the default insert to be performed. + *

    + */ + public boolean preInsert(BeanPersistRequest request); + + /** + * Prior to the update perform some action. Return true if you want the + * default functionality to continue. + *

    + * Return false if you have completely replaced the update functionality and + * do not want the default update to be performed. + *

    + */ + public boolean preUpdate(BeanPersistRequest request); + + /** + * Prior to the delete perform some action. Return true if you want the + * default functionality to continue. + *

    + * Return false if you have completely replaced the delete functionality and + * do not want the default delete to be performed. + *

    + */ + public boolean preDelete(BeanPersistRequest request); + + /** + * Called after the insert was performed. + */ + public void postInsert(BeanPersistRequest request); + + /** + * Called after the update was performed. + */ + public void postUpdate(BeanPersistRequest request); + + /** + * Called after the delete was performed. + */ + public void postDelete(BeanPersistRequest request); + + /** + * Called after every each bean is fetched and loaded from the database. You + * can override this to derive some information to set to the bean. + */ + public void postLoad(Object bean, Set includedProperties); + +} diff --git a/src/main/java/com/avaje/ebean/event/BeanPersistListener.java b/src/main/java/com/avaje/ebean/event/BeanPersistListener.java new file mode 100644 index 000000000..1b1e1f23d --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/BeanPersistListener.java @@ -0,0 +1,92 @@ +package com.avaje.ebean.event; + +import java.util.Set; + +/** + * Listens for committed bean events. + *

    + * These listen events occur after a successful commit. They also occur in a + * background thread rather than the thread used to perform the actual insert + * update or delete. In this way there is a delay between the commit and when + * the listener is notified of the event. + *

    + *

    + * For a cluster these events may need to be broadcast. Each of the inserted(), + * updated() and deleted() methods return true if you want those events to be + * broadcast to the other members of a cluster (the id values are broadcast). If + * these methods return false then the events are not broadcast. + *

    + *

    + * It is worth noting that BeanPersistListener is different in three main ways + * from BeanPersistController postXXX methods. + *

      + *
    • BeanPersistListener only sees successfully committed events. + * BeanPersistController pre and post methods occur before the commit or a + * rollback and will see events that are later rolled back
    • + *
    • BeanPersistListener runs in a background thread and will not effect the + * response time of the actual persist where as BeanPersistController code will
    • + *
    • BeanPersistListener can be notified of events from other servers in a + * cluster.
    • + *
    + *

    + *

    + * A BeanPersistListener is either found automatically via class path search or + * can be added programmatically via ServerConfiguration.addEntity(). + *

    + */ +public interface BeanPersistListener { + + /** + * Notified that a bean has been inserted locally. Return true if you want the + * cluster to be notified of the event. + * + * @param bean + * The bean that was inserted. + */ + public boolean inserted(T bean); + + /** + * Notified that a bean has been updated locally. Return true if you want the + * cluster to be notified of the event. + * + * @param bean + * The bean that was updated. + * @param updatedProperties + * the properties on the bean that where updated + */ + public boolean updated(T bean, Set updatedProperties); + + /** + * Notified that a bean has been deleted locally. Return true if you want the + * cluster to be notified of the event. + * + * @param bean + * The bean that was deleted. + */ + public boolean deleted(T bean); + + /** + * Notify that a bean was inserted on another node of the cluster. + * + * @param id + * the id value of the inserted bean + */ + public void remoteInsert(Object id); + + /** + * Notify that a bean was updated on another node of the cluster. + * + * @param id + * the id value of the updated bean. + */ + public void remoteUpdate(Object id); + + /** + * Notify that a bean was deleted on another node of the cluster. + * + * @param id + * the id value of the deleted bean. + */ + public void remoteDelete(Object id); + +} diff --git a/src/main/java/com/avaje/ebean/event/BeanPersistRequest.java b/src/main/java/com/avaje/ebean/event/BeanPersistRequest.java new file mode 100644 index 000000000..97573bcf6 --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/BeanPersistRequest.java @@ -0,0 +1,52 @@ +package com.avaje.ebean.event; + +import java.util.Set; + +import com.avaje.ebean.EbeanServer; +import com.avaje.ebean.Transaction; + +/** + * Holds the information available for a bean persist (insert, update or + * delete). + *

    + * This is made available for the BeanPersistControllers. + *

    + */ +public interface BeanPersistRequest { + + /** + * Return the server processing the request. + */ + public EbeanServer getEbeanServer(); + + /** + * Return the Transaction associated with this request. + */ + public Transaction getTransaction(); + + /** + * For an update or delete of a partially populated bean this is the set of + * loaded properties and otherwise returns null. + */ + public Set getLoadedProperties(); + + /** + * For an update this is the set of properties that where updated. + */ + public Set getUpdatedProperties(); + + /** + * Returns the bean being inserted updated or deleted. + */ + public T getBean(); + + /** + * Returns a bean containing the original values prior to the bean being + * modified. + *

    + * This is for updates only. + *

    + */ + public T getOldValues(); + +} diff --git a/src/main/java/com/avaje/ebean/event/BeanQueryAdapter.java b/src/main/java/com/avaje/ebean/event/BeanQueryAdapter.java new file mode 100644 index 000000000..183eee6e3 --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/BeanQueryAdapter.java @@ -0,0 +1,41 @@ +package com.avaje.ebean.event; + +import com.avaje.ebean.config.ServerConfig; + +/** + * Objects extending this modify queries prior their execution. + *

    + * This can be used to add expressions to a query - for example to enable + * partitioning based on the user executing the query. + *

    + *

    + * A BeanQueryAdapter is either found automatically via class path search or can + * be added programmatically via {@link ServerConfig#add(BeanQueryAdapter)}. + *

    + *

    + * Note that a BeanQueryAdapter should be thread safe (stateless) and if + * registered automatically via class path search it needs to have a default + * constructor. + *

    + */ +public interface BeanQueryAdapter { + + /** + * Return true if this adapter is interested in queries for the given entity + * type. + */ + public boolean isRegisterFor(Class cls); + + /** + * Returns an int to to control the order in which BeanQueryAdapter are + * executed when there is multiple of them registered for a given entity type + * (class). + */ + public int getExecutionOrder(); + + /** + * Modify the associated query prior to it being executed. + */ + public void preQuery(BeanQueryRequest request); + +} diff --git a/src/main/java/com/avaje/ebean/event/BeanQueryRequest.java b/src/main/java/com/avaje/ebean/event/BeanQueryRequest.java new file mode 100644 index 000000000..cee77a31b --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/BeanQueryRequest.java @@ -0,0 +1,27 @@ +package com.avaje.ebean.event; + +import com.avaje.ebean.EbeanServer; +import com.avaje.ebean.Query; +import com.avaje.ebean.Transaction; + +/** + * Holds the information available for a bean query. + */ +public interface BeanQueryRequest { + + /** + * Return the server processing the request. + */ + public EbeanServer getEbeanServer(); + + /** + * Return the Transaction associated with this request. + */ + public Transaction getTransaction(); + + /** + * Returns the query. + */ + public Query getQuery(); + +} diff --git a/src/main/java/com/avaje/ebean/event/BulkTableEvent.java b/src/main/java/com/avaje/ebean/event/BulkTableEvent.java new file mode 100644 index 000000000..386bdd07d --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/BulkTableEvent.java @@ -0,0 +1,30 @@ +package com.avaje.ebean.event; + +/** + * The bulk table event. + * + * @author Robin Bygrave + */ +public interface BulkTableEvent { + + /** + * Return the name of the table that was involved. + */ + public String getTableName(); + + /** + * Return true if rows were inserted. + */ + public boolean isInsert(); + + /** + * Return true if rows were updated. + */ + public boolean isUpdate(); + + /** + * Return true if rows were deleted. + */ + public boolean isDelete(); + +} diff --git a/src/main/java/com/avaje/ebean/event/BulkTableEventListener.java b/src/main/java/com/avaje/ebean/event/BulkTableEventListener.java new file mode 100644 index 000000000..c38beef80 --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/BulkTableEventListener.java @@ -0,0 +1,30 @@ +package com.avaje.ebean.event; + +import java.util.Set; + +import com.avaje.ebean.Ebean; + +/** + * Listen for bulk table events that occur. + *

    + * These events can be triggered via + * {@link Ebean#externalModification(String, boolean, boolean, boolean)} or + * automatically determined from Ebean bulk update statements. + *

    + * + * @author Robin Bygrave + * + */ +public interface BulkTableEventListener { + + /** + * Return the tables that this listener is interested in. + */ + public Set registeredTables(); + + /** + * Process the event. + */ + public void process(BulkTableEvent bulkTableEvent); + +} diff --git a/src/main/java/com/avaje/ebean/event/ServerConfigStartup.java b/src/main/java/com/avaje/ebean/event/ServerConfigStartup.java new file mode 100644 index 000000000..8b097a7d0 --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/ServerConfigStartup.java @@ -0,0 +1,21 @@ +package com.avaje.ebean.event; + +import com.avaje.ebean.config.ServerConfig; + +/** + * Used to configure the server on startup. + *

    + * Provides a simple way to construct and register multiple listeners and + * adapters that need shared services without using DI. + *

    + * + * @author Robin Bygrave + */ +public interface ServerConfigStartup { + + /** + * On starting configure the ServerConfig. + */ + public void onStart(ServerConfig serverConfig); + +} diff --git a/src/main/java/com/avaje/ebean/event/TransactionEventListener.java b/src/main/java/com/avaje/ebean/event/TransactionEventListener.java new file mode 100644 index 000000000..a82e70f68 --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/TransactionEventListener.java @@ -0,0 +1,18 @@ +package com.avaje.ebean.event; + +import com.avaje.ebean.Transaction; + +/** + * Used to get notified about commit or rollback of a transaction + */ +public interface TransactionEventListener { + /** + * Called after the transaction has been committed + */ + public void postTransactionCommit(Transaction tx); + + /** + * Called after the transaction has been rolled back + */ + public void postTransactionRollback(Transaction tx, Throwable cause); +} diff --git a/src/main/java/com/avaje/ebean/event/TransactionEventListenerAdapter.java b/src/main/java/com/avaje/ebean/event/TransactionEventListenerAdapter.java new file mode 100644 index 000000000..d9f0e3d20 --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/TransactionEventListenerAdapter.java @@ -0,0 +1,18 @@ +package com.avaje.ebean.event; + +import com.avaje.ebean.Transaction; + +/** + * A no operation implementation of TransactionEventListener. Objects extending + * this need to only override the methods they want to. + */ +public abstract class TransactionEventListenerAdapter implements TransactionEventListener { + + public void postTransactionCommit(Transaction tx) { + // do nothing by default + } + + public void postTransactionRollback(Transaction tx, Throwable cause) { + // do nothing by default + } +} diff --git a/src/main/java/com/avaje/ebean/event/package.html b/src/main/java/com/avaje/ebean/event/package.html new file mode 100644 index 000000000..83dea44fa --- /dev/null +++ b/src/main/java/com/avaje/ebean/event/package.html @@ -0,0 +1,10 @@ + + + + Persist and Query Event Controllers and Listeners + + +Persist and Query Event Controllers and Listeners + + + \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/meta/MetaAutoFetchStatistic.java b/src/main/java/com/avaje/ebean/meta/MetaAutoFetchStatistic.java new file mode 100644 index 000000000..e7327d835 --- /dev/null +++ b/src/main/java/com/avaje/ebean/meta/MetaAutoFetchStatistic.java @@ -0,0 +1,228 @@ +package com.avaje.ebean.meta; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Transient; + +import com.avaje.ebean.bean.ObjectGraphOrigin; + +/** + * Statistics collected by AutoFetch profiling. + */ +@Entity +public class MetaAutoFetchStatistic implements Serializable { + + private static final long serialVersionUID = -6640406753257176803L; + + @Id + private String id; + + private ObjectGraphOrigin origin; + + private String beanType; + + private int counter; + + @Transient + private List queryStats; + + @Transient + private List nodeUsageStats; + + public MetaAutoFetchStatistic() { + } + + public MetaAutoFetchStatistic(ObjectGraphOrigin origin, int counter, List queryStats, + List nodeUsageStats) { + + this.origin = origin; + this.beanType = origin == null ? null : origin.getBeanType(); + this.id = origin == null ? null : origin.getKey(); + this.counter = counter; + this.queryStats = queryStats; + this.nodeUsageStats = nodeUsageStats; + } + + /** + * This is the query point key. + */ + public String getId() { + return id; + } + + /** + * Return the bean type. + */ + public String getBeanType() { + return beanType; + } + + /** + * Return the query point. + */ + public ObjectGraphOrigin getOrigin() { + return origin; + } + + /** + * Return the number of profiled queries the statistics is based on. + */ + public int getCounter() { + return counter; + } + + /** + * Return the query execution statistics. + */ + public List getQueryStats() { + return queryStats; + } + + /** + * Return the node usage statistics. + */ + public List getNodeUsageStats() { + return nodeUsageStats; + } + + /** + * FIXME: This will likely be deprecated in favour of a separate object graph + * cost. + */ + public static class QueryStats implements Serializable { + + private static final long serialVersionUID = -5517935732867671387L; + + private final String path; + + private final int exeCount; + + private final int totalBeanLoaded; + + private final int totalMicros; + + public QueryStats(String path, int exeCount, int totalBeanLoaded, int totalMicros) { + this.path = path; + this.exeCount = exeCount; + this.totalBeanLoaded = totalBeanLoaded; + this.totalMicros = totalMicros; + } + + /** + * Return the path. This is empty string for the origin query and otherwise + * the path for the associated lazy loading queries. + */ + public String getPath() { + return path; + } + + /** + * The number of queries executed. + */ + public int getExeCount() { + return exeCount; + } + + /** + * The total number of beans loaded by the query. + */ + public int getTotalBeanLoaded() { + return totalBeanLoaded; + } + + /** + * The total time in microseconds of the queries. + */ + public int getTotalMicros() { + return totalMicros; + } + + public String toString() { + long avgMicros = exeCount == 0 ? 0 : totalMicros / exeCount; + + return "queryExe path[" + path + "] count[" + exeCount + "] totalBeansLoaded[" + + totalBeanLoaded + "] avgMicros[" + avgMicros + "] totalMicros[" + totalMicros + + "]"; + } + } + + /** + * Collects usages statistics for a given node in the object graph. + */ + public static class NodeUsageStats implements Serializable { + + private static final long serialVersionUID = 1786787832374844739L; + + private final String path; + + private final int profileCount; + + private final int profileUsedCount; + + private final String[] usedProperties; + + public NodeUsageStats(String path, int profileCount, int profileUsedCount, + String[] usedProperties) { + this.path = path == null ? "" : path; + this.profileCount = profileCount; + this.profileUsedCount = profileUsedCount; + this.usedProperties = usedProperties; + } + + /** + * Return the path. This is empty string for the origin and otherwise the + * path for the associated nodes. + */ + public String getPath() { + return path; + } + + /** + * The number of profiled beans for this node. + */ + public int getProfileCount() { + return profileCount; + } + + /** + * The number of profiled beans that where actually used for this node. + *

    + * The difference between profiled and used could show uneven traversal of + * the object graph. UI paging through results means the traversal for the + * first x beans can be much higher than the last x beans. + *

    + */ + public int getProfileUsedCount() { + return profileUsedCount; + } + + /** + * The properties used at this node. + */ + public String[] getUsedProperties() { + return usedProperties; + } + + /** + * Return the properties as a Set rather than an Array. + */ + public Set getUsedPropertiesSet() { + LinkedHashSet s = new LinkedHashSet(); + for (int i = 0; i < usedProperties.length; i++) { + s.add(usedProperties[i]); + } + return s; + } + + public String toString() { + return "path[" + path + "] profileCount[" + profileCount + "] used[" + profileUsedCount + + "] props" + Arrays.toString(usedProperties); + } + } +} diff --git a/src/main/java/com/avaje/ebean/meta/MetaAutoFetchTunedQueryInfo.java b/src/main/java/com/avaje/ebean/meta/MetaAutoFetchTunedQueryInfo.java new file mode 100644 index 000000000..9549e2a03 --- /dev/null +++ b/src/main/java/com/avaje/ebean/meta/MetaAutoFetchTunedQueryInfo.java @@ -0,0 +1,119 @@ +package com.avaje.ebean.meta; + +import java.io.Serializable; + +import javax.persistence.Entity; +import javax.persistence.Id; + +import com.avaje.ebean.bean.ObjectGraphOrigin; + +/** + * "Tuned fetch" information used by AutoFetch. + *

    + * Note that the queryPoint is effectively the Id field for this bean. + *

    + *

    + * The queryPoint identifies both the query and call stack. + *

    + */ +@Entity +public class MetaAutoFetchTunedQueryInfo implements Serializable { + + private static final long serialVersionUID = 3119991928889170215L; + + @Id + private String id; + + private String beanType; + + /** + * The profile query point (call stack and query). + */ + private ObjectGraphOrigin origin; + + /** + * The tuned query details with joins and properties. + */ + private String tunedDetail; + + /** + * The number of times profiling has been collected for this query point. + */ + private int profileCount; + + /** + * The number of queries tuned by this info. + */ + private int tunedCount; + + private long lastTuneTime; + + public MetaAutoFetchTunedQueryInfo() { + + } + + public MetaAutoFetchTunedQueryInfo(final ObjectGraphOrigin origin, String tunedDetail, + int profileCount, int tunedCount, long lastTuneTime) { + + this.origin = origin; + this.beanType = origin == null ? null : origin.getBeanType(); + this.id = origin == null ? null : origin.getKey(); + this.tunedDetail = tunedDetail; + this.profileCount = profileCount; + this.tunedCount = tunedCount; + this.lastTuneTime = lastTuneTime; + } + + /** + * Return the query point key. + */ + public String getId() { + return id; + } + + /** + * Return the type of bean this is tuned for. + */ + public String getBeanType() { + return beanType; + } + + /** + * Return the query point. + */ + public ObjectGraphOrigin getOrigin() { + return origin; + } + + /** + * The tuned query detail in string form. + */ + public String getTunedDetail() { + return tunedDetail; + } + + /** + * The number of profiled queries the tuned query is based on. + */ + public int getProfileCount() { + return profileCount; + } + + /** + * Return the number of queries tuned. + */ + public int getTunedCount() { + return tunedCount; + } + + /** + * Return the time of the last tune (that changed the query). + */ + public long getLastTuneTime() { + return lastTuneTime; + } + + public String toString() { + return "origin[" + origin + "] query[" + tunedDetail + "] profileCount[" + profileCount + "]"; + } +} diff --git a/src/main/java/com/avaje/ebean/meta/MetaQueryStatistic.java b/src/main/java/com/avaje/ebean/meta/MetaQueryStatistic.java new file mode 100644 index 000000000..6f0106536 --- /dev/null +++ b/src/main/java/com/avaje/ebean/meta/MetaQueryStatistic.java @@ -0,0 +1,172 @@ +package com.avaje.ebean.meta; + +import java.io.Serializable; + +import javax.persistence.Entity; + +/** + * Query execution statistics Meta data. + */ +@Entity +public class MetaQueryStatistic implements Serializable { + + private static final long serialVersionUID = -8746524372894472583L; + + boolean autofetchTuned; + + String beanType; + + /** + * The original query plan hash (calculated prior to autofetch tuning). + */ + int origQueryPlanHash; + + /** + * The final query plan hash (calculated after to autofetch tuning). + */ + int finalQueryPlanHash; + + String sql; + + int executionCount; + + int totalLoadedBeans; + + int totalTimeMicros; + + long collectionStart; + + long lastQueryTime; + + int avgTimeMicros; + + int avgLoadedBeans; + + public MetaQueryStatistic() { + + } + + /** + * Create a MetaQueryStatistic. + */ + public MetaQueryStatistic(boolean autofetchTuned, String beanType, int plan, String sql, + int executionCount, int totalLoadedBeans, int totalTimeMicros, long collectionStart, + long lastQueryTime) { + + this.autofetchTuned = autofetchTuned; + this.beanType = beanType; + this.finalQueryPlanHash = plan; + this.sql = sql; + this.executionCount = executionCount; + this.totalLoadedBeans = totalLoadedBeans; + this.totalTimeMicros = totalTimeMicros; + this.collectionStart = collectionStart; + + this.lastQueryTime = lastQueryTime; + this.avgTimeMicros = executionCount == 0 ? 0 : totalTimeMicros / executionCount; + this.avgLoadedBeans = executionCount == 0 ? 0 : totalLoadedBeans / executionCount; + } + + public String toString() { + return "type=" + beanType + " tuned:" + autofetchTuned + " origHash=" + origQueryPlanHash + + " count=" + executionCount + " avgMicros=" + getAvgTimeMicros(); + } + + /** + * Return true if this query plan was built for Autofetch tuned queries. + */ + public boolean isAutofetchTuned() { + return autofetchTuned; + } + + /** + * Return the original query plan hash (calculated prior to autofetch tuning). + *

    + * This will return 0 if there is no autofetch profiling or tuning on this + * query. + *

    + */ + public int getOrigQueryPlanHash() { + return origQueryPlanHash; + } + + /** + * Return the queryPlanHash value. This is unique for a given query plan. + */ + public int getFinalQueryPlanHash() { + return finalQueryPlanHash; + } + + /** + * Return the bean type. + */ + public String getBeanType() { + return beanType; + } + + /** + * Return the sql executed. + */ + public String getSql() { + return sql; + } + + /** + * Return the total number of queries executed. + */ + public int getExecutionCount() { + return executionCount; + } + + /** + * Return the total number of beans loaded by the queries. + *

    + * This excludes background fetching. + *

    + */ + public int getTotalLoadedBeans() { + return totalLoadedBeans; + } + + /** + * Return the number of times this query was executed. + */ + public int getTotalTimeMicros() { + return totalTimeMicros; + } + + /** + * Return the time collection started. + */ + public long getCollectionStart() { + return collectionStart; + } + + /** + * Return the time of the last query executed using this plan. + */ + public long getLastQueryTime() { + return lastQueryTime; + } + + /** + * Return the average query execution time in microseconds. + *

    + * This excludes background fetching. + *

    + */ + public int getAvgTimeMicros() { + return avgTimeMicros; + } + + /** + * Return the average number of bean loaded per query. + *

    + * This excludes background fetching. + *

    + */ + public int getAvgLoadedBeans() { + return avgLoadedBeans; + } + +} diff --git a/src/main/java/com/avaje/ebean/meta/package.html b/src/main/java/com/avaje/ebean/meta/package.html new file mode 100644 index 000000000..81281f219 --- /dev/null +++ b/src/main/java/com/avaje/ebean/meta/package.html @@ -0,0 +1,17 @@ + + +Entity Beans for getting "Meta" data from Ebean + + +Entity Beans for getting "Meta" data from Ebean +

    +You can query these entity beans to get "meta" data from Ebean. +This includes things like query execution statistics. +

    +
    +// fetch the meta data that controls autoFetch query tuning
    +Query query = Ebean.createQuery(MetaAutoFetchTunedFetch.class);
    +List list = query.findList();
    +
    + + \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/overview.html b/src/main/java/com/avaje/ebean/overview.html new file mode 100644 index 000000000..85d597896 --- /dev/null +++ b/src/main/java/com/avaje/ebean/overview.html @@ -0,0 +1,133 @@ + + + Ebean API + + +Ebean Object Relational Mapping (start at Ebean +or EbeanServer). + + +

    Ebean

    +

    +Provides the main API for fetching and persisting beans with Ebean. +

    +

    +For a full description of the query language refer to Query. +

    +

    +  +

    +
    +

    +EXAMPLE 1: Simple fetch +

    +
    +// fetch order 10
    +Order order = Ebean.find(Order.class, 10);
    +
    + +

    +EXAMPLE 2: Fetch an Object with associations +

    +
    +// fetch Customer 7 including their billing and shipping addresses
    +Customer customer = Ebean.find(Customer.class)
    +    .fetch("billingAddress");
    +    .fetch("shippingAddress");
    +    .setId(7)
    +    .findUnique();
    +	
    +		
    +Address billAddr = customer.getBillingAddress();
    +Address shipAddr = customer.getShippingAddress();
    +
    + +

    +EXAMPLE 3: Fetch a list of Objects with associations +

    +
    +// Note: This example shows a "Partial Object".
    +//       For the product objects associated with the 
    +//       order details only the product id and name is
    +//       fetched (the product objects are partially populated).
    +		
    +// fetch orders for customer.id = 2
    +List<Order> orderList = Ebean.find(Order.class);
    +    .fetch("customer")
    +    .fetch("customer.shippingAddress")
    +    .fetch("details")
    +    .fetch("details.product","name")
    +    .where().eq("customer.id",2)
    +    .findList();
    +
    +
    +// Note: Only the product id and name is fetched for the
    +//       product details. This is referred to as a 
    +//       "Partial Object" (one that is partially populated).  
    +
    +
    +// code that traverses the object graph...
    +
    +Order order = orderList.get(0);
    +Customer customer = order.getCustomer();	
    +Address shipAddr = customer.getShippingAddress();
    +
    +List<OrderDetail> details = order.getDetails();
    +OrderDetail detail = details.get(0);
    +Product product = detail.getProduct();
    +String productName = product.getName();
    +
    +
    + +

    +EXAMPLE 4: Create and save an Order +

    +
    +// get a Customer reference so we don't hit the database
    +Customer custRef = Ebean.getReference(Customer.class, 7);
    +
    +// create a new Order object
    +Order newOrder = new Order();
    +newOrder.setStatus(Order.Status.NEW);
    +newOrder.setCustomer(custRef);
    +
    +ArrayList orderLines = new ArrayList();
    +newOrder.setLines(orderLines);
    +...
    +
    +// add a line to the order
    +Product prodRef = Ebean.getReference(Product.class, 41);
    +OrderLine line = new OrderLine();
    +line.setProduct(prodRef);
    +line.setQuantity(10);
    +orderLines.add(line);
    +...
    +
    +// save the order and its lines in a single transaction
    +// NB: assumes CascadeType.PERSIST is set on the order lines association
    +Ebean.save(newOrder);
    +
    +
    + +

    +EXAMPLE 5: Use another database +

    +
    +// Get access to the Human Resources EbeanServer/Database
    +EbeanServer hrServer = Ebean.getServer("HR");
    +                                    
    +                              
    +// fetch contact 3 from the HR database
    +Contact contact = hrServer.find(Contact.class, 3);
    +                                    
    +contact.setStatus(Contact.Status.INACTIVE);
    +...
    +                                    
    +// save the contact back to the HR database
    +hrServer.save(contact); 	
    +
    +
    + + + + \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/package.html b/src/main/java/com/avaje/ebean/package.html new file mode 100644 index 000000000..f0ffd4c74 --- /dev/null +++ b/src/main/java/com/avaje/ebean/package.html @@ -0,0 +1,87 @@ + + + Ebean core API + + +Core API (see Ebean and EbeanServer). + +

    Ebean

    +

    +Provides the main API for fetching and persisting beans with eBean. +

    + +
    +	// EXAMPLE 1: Simple fetch
    +	//========================
    +
    +	// fetch order 10
    +	Order order = Ebean.find(Order.class, 10);
    +
    +
    +
    +	// EXAMPLE 2: Fetch an Object with associations
    +	//=============================================
    +	
    +	// fetch Customer 7 including their billing and shipping addresses
    +	Customer customer = 
    +		Ebean.find(Customer.class)
    +			.setId(7)
    +			.fetch("billingAddress")
    +			.fetch("shippingAddress")
    +			.findUnique();	
    +	
    +	Address billAddr = customer.getBillingAddress();
    +	Address shipAddr = customer.getShippingAddress();
    +
    +
    +
    +
    +	// EXAMPLE 3: Create and save an Order
    +	//=====================================
    +	
    +	// get a Customer reference so we don't hit the database
    +	Customer custRef = Ebean.getReference(Customer.class, 7);
    +
    +	// create a new Order object
    +	Order newOrder = new Order();
    +	newOrder.setStatus(Order.Status.NEW);
    +	newOrder.setCustomer(custRef);
    +	
    +	ArrayList orderLines = new ArrayList();
    +	newOrder.setLines(orderLines);
    +	...
    +
    +	// add a line to the order
    +	Product prodRef = Ebean.getReference(Product.class, 41);
    +	OrderLine line = new OrderLine();
    +	line.setProduct(prodRef);
    +	line.setQuantity(10);
    +	orderLines.add(line);
    +	...
    +
    +	// save the order and its lines in a single transaction
    +	// NB: assumes CascadeType.PERSIST is set on the order lines association
    +	Ebean.save(newOrder);
    +
    +
    +
    +	// EXAMPLE 4: Use another database
    +	//=================================
    +
    +	// Get access to the Human Resources EbeanServer/Database
    +	EbeanServer hrServer = Ebean.getServer("HR");
    +                                        
    +                                  
    +	// fetch contact 3 from the HR database
    +	Contact contact = hrServer.find(Contact.class, 3);
    +                                        
    +	contact.setStatus(Contact.Status.INACTIVE);
    +	...
    +                                        
    +	// save the contact back to the HR database
    +	hrServer.save(contact); 
    +	
    +
    + + + \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/stylesheet.css b/src/main/java/com/avaje/ebean/stylesheet.css new file mode 100644 index 000000000..634ca5705 --- /dev/null +++ b/src/main/java/com/avaje/ebean/stylesheet.css @@ -0,0 +1,42 @@ +/* Javadoc style sheet */ + +/* Define colors, fonts and other style attributes here to override the defaults */ + +/* Page background color */ +body { +background-color: #FFFFFF; +font-family: Arial, Helvetica, sans-serif; +font-size:10pt; +} + +pre.code { + padding:1em; + margin-left:2em; + border: 1px solid #ccc; + background-color: #eee; +} + +/* Headings */ +h1 { font-size: 14pt } + +#overviewexamples h3 {font-size:10pt } + +/* Table colors */ +.TableHeadingColor { background: #CCCCFF } /* Dark mauve */ +.TableSubHeadingColor { background: #EEEEFF } /* Light mauve */ +.TableRowColor { background: #FFFFFF } /* White */ + +/* Font used in left-hand frame lists */ +.FrameTitleFont { font-size: 12pt; font-family: Helvetica, Arial, sans-serif } +.FrameHeadingFont { font-size: 10pt; font-family: Helvetica, Arial, sans-serif } +.FrameItemFont { font-size: 10pt; font-family: Helvetica, Arial, sans-serif } + +/* Navigation bar fonts and colors */ +.NavBarCell1 { background-color:#EEEEFF;} /* Light mauve */ +.NavBarCell1Rev { background-color:#00008B;} /* Dark Blue */ +.NavBarFont1 { font-family: Arial, Helvetica, sans-serif; color:#000000;} +.NavBarFont1Rev { font-family: Arial, Helvetica, sans-serif; color:#FFFFFF;} + +.NavBarCell2 { font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF;} +.NavBarCell3 { font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF;} + diff --git a/src/main/java/com/avaje/ebean/text/PathProperties.java b/src/main/java/com/avaje/ebean/text/PathProperties.java new file mode 100644 index 000000000..bd3b17817 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/PathProperties.java @@ -0,0 +1,254 @@ +package com.avaje.ebean.text; + +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +import com.avaje.ebean.Query; + +/** + * This is a Tree like structure of paths and properties that can be used for + * defining which parts of an object graph to render in JSON or XML, and can + * also be used to define which parts to select and fetch for an ORM query. + *

    + * It provides a way of parsing a string representation of nested path + * properties and applying that to both what to fetch (ORM query) and what to + * render (JAX-RS JSON / XML). + *

    + * + * @author rbygrave + * + */ +public class PathProperties { + + private final Map pathMap; + + private final Props rootProps; + + /** + * Parse and return a PathProperties from nested string format like + * (a,b,c(d,e),f(g)) where "c" is a path containing "d" and "e" and "f" is a + * path containing "g" and the root path contains "a","b","c" and "f". + */ + public static PathProperties parse(String source) { + return PathPropertiesParser.parse(source); + } + + /** + * Construct an empty PathProperties. + */ + public PathProperties() { + this.rootProps = new Props(this, null, null); + this.pathMap = new LinkedHashMap(); + this.pathMap.put(null, rootProps); + } + + /** + * Construct for creating copy. + */ + private PathProperties(PathProperties orig) { + this.rootProps = orig.rootProps.copy(this); + this.pathMap = new LinkedHashMap(orig.pathMap.size()); + Set> entrySet = orig.pathMap.entrySet(); + for (Entry e : entrySet) { + pathMap.put(e.getKey(), e.getValue().copy(this)); + } + } + + /** + * Create a copy of this instance so that it can be modified. + *

    + * For example, you may want to create a copy to add extra properties to a + * path so that they are fetching in a ORM query but perhaps not rendered by + * default. That is, use a PathProperties for JSON or XML rendering, but + * create a copy, add some extra properties and then use that copy to define + * an ORM query. + *

    + */ + public PathProperties copy() { + return new PathProperties(this); + } + + /** + * Return true if there are no paths defined. + */ + public boolean isEmpty() { + return pathMap.isEmpty(); + } + + public String toString() { + return pathMap.toString(); + } + + /** + * Return true if the path is defined and has properties. + */ + public boolean hasPath(String path) { + Props props = pathMap.get(path); + return props != null && !props.isEmpty(); + } + + /** + * Get the properties for a given path. + */ + public Set get(String path) { + Props props = pathMap.get(path); + return props == null ? null : props.getProperties(); + } + + public void addToPath(String path, String property) { + Props props = pathMap.get(path); + if (props == null) { + props = new Props(this, null, path); + pathMap.put(path, props); + } + props.getProperties().add(property); + } + + /** + * Set the properties for a given path. + */ + public void put(String path, Set properties) { + pathMap.put(path, new Props(this, null, path, properties)); + } + + /** + * Remove a path returning the properties set for that path. + */ + public Set remove(String path) { + Props props = pathMap.remove(path); + return props == null ? null : props.getProperties(); + } + + /** + * Return a shallow copy of the paths. + */ + public Set getPaths() { + return new LinkedHashSet(pathMap.keySet()); + } + + public Collection getPathProps() { + return pathMap.values(); + } + + /** + * Apply these path properties as fetch paths to the query. + */ + public void apply(Query query) { + + for (Entry entry : pathMap.entrySet()) { + String path = entry.getKey(); + String props = entry.getValue().getPropertiesAsString(); + + if (path == null || path.length() == 0) { + query.select(props); + } else { + query.fetch(path, props); + } + } + } + + protected Props getRootProperties() { + return rootProps; + } + + public static class Props { + + private final PathProperties owner; + + private final String parentPath; + private final String path; + + private final Set propSet; + + private Props(PathProperties owner, String parentPath, String path, Set propSet) { + this.owner = owner; + this.path = path; + this.parentPath = parentPath; + this.propSet = propSet; + } + + private Props(PathProperties owner, String parentPath, String path) { + this(owner, parentPath, path, new LinkedHashSet()); + } + + /** + * Create a shallow copy of this Props instance. + */ + public Props copy(PathProperties newOwner) { + return new Props(newOwner, parentPath, path, new LinkedHashSet(propSet)); + } + + public String getPath() { + return path; + } + + public String toString() { + return propSet.toString(); + } + + public boolean isEmpty() { + return propSet.isEmpty(); + } + + /** + * Return the properties for this property set. + */ + public Set getProperties() { + return propSet; + } + + /** + * Return the properties as a comma delimited string. + */ + public String getPropertiesAsString() { + + StringBuilder sb = new StringBuilder(); + + Iterator it = propSet.iterator(); + boolean hasNext = it.hasNext(); + while (hasNext) { + sb.append(it.next()); + hasNext = it.hasNext(); + if (hasNext) { + sb.append(","); + } + } + return sb.toString(); + } + + /** + * Return the parent path + */ + protected Props getParent() { + return owner.pathMap.get(parentPath); + } + + /** + * Add a child Property set. + */ + protected Props addChild(String subpath) { + + subpath = subpath.trim(); + addProperty(subpath); + + // build the subpath + String p = path == null ? subpath : path + "." + subpath; + Props nested = new Props(owner, path, p); + owner.pathMap.put(p, nested); + return nested; + } + + /** + * Add a properties to include for this path. + */ + protected void addProperty(String property) { + propSet.add(property.trim()); + } + } + +} diff --git a/src/main/java/com/avaje/ebean/text/PathPropertiesParser.java b/src/main/java/com/avaje/ebean/text/PathPropertiesParser.java new file mode 100644 index 000000000..d6bef851e --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/PathPropertiesParser.java @@ -0,0 +1,128 @@ +package com.avaje.ebean.text; + +/** + * Parses Uri segments like :(id,name,shippingAddress(*),contacts(*)) so that + * the response can be customised for performance. + * + * @author rbygrave + */ +class PathPropertiesParser { + + // :(a,b,c(d,e,f)) + + private final PathProperties pathProps; + + private final String source; + + private final char[] chars; + + private final int eof; + + private int pos; + private int startPos; + + private PathProperties.Props currentPathProps; + + /** + * Use {@link PathProperties#parse(String)}. + */ + static PathProperties parse(String source) { + return new PathPropertiesParser(source).pathProps; + } + + private PathPropertiesParser(String src) { + + if (src.startsWith(":")) { + src = src.substring(1); + } + this.pathProps = new PathProperties(); + this.source = src; + this.chars = src.toCharArray(); + this.eof = chars.length; + + if (eof > 0) { + currentPathProps = pathProps.getRootProperties(); + parse(); + } + } + + private String getPath() { + do { + char c1 = chars[pos++]; + switch (c1) { + case '(': + return currentWord(); + default: + } + } while (pos < eof); + throw new RuntimeException("Hit EOF while reading sectionTitle from " + startPos); + } + + private void parse() { + + do { + String path = getPath(); + pushPath(path); + parseSection(); + + } while (pos < eof); + } + + private void parseSection() { + do { + char c1 = chars[pos++]; + switch (c1) { + case '(': + addSubpath(); + break; + case ',': + addCurrentProperty(); + break; + case ':': + // start new section + startPos = pos; + return; + case ')': + // end of section + addCurrentProperty(); + popSubpath(); + break; + default: + } + + } while (pos < eof); + } + + private void addSubpath() { + pushPath(currentWord()); + } + + private void addCurrentProperty() { + String w = currentWord(); + if (w.length() > 0) { + currentPathProps.addProperty(w); + } + } + + private String currentWord() { + if (startPos == pos) { + return ""; + } + String currentWord = source.substring(startPos, pos - 1); + startPos = pos; + return currentWord; + } + + private void pushPath(String title) { + + if (!"".equals(title)) { + currentPathProps = currentPathProps.addChild(title); + } + } + + private void popSubpath() { + + currentPathProps = currentPathProps.getParent(); + } + +} diff --git a/src/main/java/com/avaje/ebean/text/StringFormatter.java b/src/main/java/com/avaje/ebean/text/StringFormatter.java new file mode 100644 index 000000000..62e760c88 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/StringFormatter.java @@ -0,0 +1,17 @@ +package com.avaje.ebean.text; + +/** + * Convert an Object value into a String value. + *

    + * Basic interface to support CSV, JSON and XML processing. + *

    + * + * @author rbygrave + */ +public interface StringFormatter { + + /** + * Convert an Object value into a String value. + */ + public String format(Object value); +} diff --git a/src/main/java/com/avaje/ebean/text/StringParser.java b/src/main/java/com/avaje/ebean/text/StringParser.java new file mode 100644 index 000000000..9eb239ea8 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/StringParser.java @@ -0,0 +1,17 @@ +package com.avaje.ebean.text; + +/** + * Convert a String value into an Object value. + *

    + * Basic interface to support CSV, JSON and XML processing. + *

    + * + * @author rbygrave + */ +public interface StringParser { + + /** + * Convert a String value into an Object value. + */ + public Object parse(String value); +} diff --git a/src/main/java/com/avaje/ebean/text/TextException.java b/src/main/java/com/avaje/ebean/text/TextException.java new file mode 100644 index 000000000..dbaf83cb4 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/TextException.java @@ -0,0 +1,32 @@ +package com.avaje.ebean.text; + +/** + * An exception occured typically in processing CSV, JSON or XML. + * + * @author rbygrave + */ +public class TextException extends RuntimeException { + + private static final long serialVersionUID = 1601310159486033148L; + + /** + * Construct with an error message. + */ + public TextException(String msg) { + super(msg); + } + + /** + * Construct with a message and cause. + */ + public TextException(String msg, Exception e) { + super(msg, e); + } + + /** + * Construct with a cause. + */ + public TextException(Exception e) { + super(e); + } +} diff --git a/src/main/java/com/avaje/ebean/text/TimeStringParser.java b/src/main/java/com/avaje/ebean/text/TimeStringParser.java new file mode 100644 index 000000000..cb212ccaa --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/TimeStringParser.java @@ -0,0 +1,56 @@ +package com.avaje.ebean.text; + +import java.sql.Time; + +/** + * Parser for TIME types that supports both HH:mm:ss and HH:mm. + * + * @author rbygrave + */ +public final class TimeStringParser implements StringParser { + + private static final TimeStringParser SHARED = new TimeStringParser(); + + /** + * Return a shared instance as this is thread safe. + */ + public static TimeStringParser get() { + return SHARED; + } + + /** + * Parse the String supporting both HH:mm:ss and HH:mm formats. + */ + @SuppressWarnings("deprecation") + public Object parse(String value) { + if (value == null || value.trim().length() == 0) { + return null; + } + + String s = value.trim(); + int minute; + int second; + int firstColon = s.indexOf(':'); + int secondColon = s.indexOf(':', firstColon + 1); + + if (firstColon == -1) { + throw new java.lang.IllegalArgumentException("No ':' in value [" + s + "]"); + } + try { + int hour = Integer.parseInt(s.substring(0, firstColon)); + if (secondColon == -1) { + minute = Integer.parseInt(s.substring(firstColon + 1, s.length())); + second = 0; + } else { + minute = Integer.parseInt(s.substring(firstColon + 1, secondColon)); + second = Integer.parseInt(s.substring(secondColon + 1)); + } + + return new Time(hour, minute, second); + + } catch (NumberFormatException e) { + throw new java.lang.IllegalArgumentException("Number format Error parsing time [" + s + "] " + + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/text/csv/CsvCallback.java b/src/main/java/com/avaje/ebean/text/csv/CsvCallback.java new file mode 100644 index 000000000..44125a155 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/csv/CsvCallback.java @@ -0,0 +1,89 @@ +package com.avaje.ebean.text.csv; + +import com.avaje.ebean.EbeanServer; + +/** + * Provides callback methods for customisation of CSV processing. + *

    + * You can provide your own CsvCallback implementation to customise the CSV + * processing. It is expected that the DefaultCsvCallback provides a good base + * class that you can extend. + *

    + * + * @author rbygrave + * + * @param + */ +public interface CsvCallback { + + /** + * The processing is about to begin. + *

    + * Typically the callback will create a transaction, set batch mode, batch + * size etc. + *

    + */ + public void begin(EbeanServer server); + + /** + * Read the header row. + *

    + * This is only called if {@link CsvReader#setHasHeader(boolean,boolean)} has + * been set to true. + *

    + * + * @param line + * the header line content. + */ + public void readHeader(String[] line); + + /** + * Check that the row should be processed - return true to process the row or + * false to ignore the row. Gives ability to handle bad data... empty rows etc + * and ignore it rather than fail. + */ + public boolean processLine(int row, String[] line); + + /** + * Called for each bean after it has been loaded from the CSV content. + *

    + * This allows you to process the bean however you like. + *

    + *

    + * When you use a CsvCallback the CsvReader *WILL NOT* create a transaction + * and will not save the bean for you. You have complete control and must do + * these things yourself (if that is want you want). + *

    + * + * @param row + * the index of the content being processed + * @param line + * the content that has been used to load the bean + * @param bean + * the entity bean after it has been loaded from the csv content + */ + public void processBean(int row, String[] line, T bean); + + /** + * The processing has ended successfully. + *

    + * Typically the callback will commit the transaction. + *

    + */ + public void end(int row); + + /** + * The processing has ended due to an error. + *

    + * This gives the callback the opportunity to rollback the transaction if one + * was created. + *

    + * + * @param row + * the row that the error has occured on + * @param e + * the error that occured + */ + public void endWithError(int row, Exception e); + +} diff --git a/src/main/java/com/avaje/ebean/text/csv/CsvReader.java b/src/main/java/com/avaje/ebean/text/csv/CsvReader.java new file mode 100644 index 000000000..2052648f2 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/csv/CsvReader.java @@ -0,0 +1,183 @@ +package com.avaje.ebean.text.csv; + +import java.io.Reader; +import java.util.Locale; + +import com.avaje.ebean.text.StringParser; + +/** + * Reads CSV data turning it into object graphs that you can be saved (inserted) + * or processed yourself. + * + *

    + * This first example doesn't use a {@link CsvCallback} and this means it will + * automatically create a transaction, save the customers and commit the + * transaction when successful. + *

    + * + *
    + * try {
    + *   File f = new File("src/test/resources/test1.csv");
    + * 
    + *   FileReader reader = new FileReader(f);
    + * 
    + *   CsvReader<Customer> csvReader = Ebean.createCsvReader(Customer.class);
    + * 
    + *   csvReader.setPersistBatchSize(20);
    + * 
    + *   csvReader.addProperty("status");
    + *   // ignore the next property
    + *   csvReader.addIgnore();
    + *   csvReader.addProperty("name");
    + *   csvReader.addDateTime("anniversary", "dd-MMM-yyyy");
    + *   csvReader.addProperty("billingAddress.line1");
    + *   csvReader.addProperty("billingAddress.city");
    + *   csvReader.addReference("billingAddress.country.code");
    + * 
    + *   csvReader.process(reader);
    + * 
    + * } catch (Exception e) {
    + *   throw new RuntimeException(e);
    + * }
    + * 
    + * + * @author rbygrave + * + * @param + * the entity bean type + */ +public interface CsvReader { + + /** + * Explicitly set the default Locale. + */ + public void setDefaultLocale(Locale defaultLocale); + + /** + * Set the default format to use for Time types. + */ + public void setDefaultTimeFormat(String defaultTimeFormat); + + /** + * Set the default format to use for Date types. + */ + public void setDefaultDateFormat(String defaultDateFormat); + + /** + * Set the default format to use for Timestamp types. + */ + public void setDefaultTimestampFormat(String defaultTimestampFormat); + + /** + * Set the batch size for using JDBC statement batching. + *

    + * By default this is set to 20 and setting this to 1 will disable the use of + * JDBC statement batching. + *

    + */ + public void setPersistBatchSize(int persistBatchSize); + + /** + * Set to true if there is a header row that should be ignored. + *

    + * If addPropertiesFromHeader is true then all the properties are added using + * the default time,date and timestamp formats. + *

    + * If you have a mix of dateTime formats you can not use this method and must + * add the properties yourself. + *

    + */ + public void setHasHeader(boolean hasHeader, boolean addPropertiesFromHeader); + + /** + * Same as setHasHeader(true,true); + *

    + * This will use a header to define all the properties to load using the + * default formats for time, date and datetime types. + *

    + */ + public void setAddPropertiesFromHeader(); + + /** + * Same as setHasHeader(true, false); + *

    + * This indicates that there is a header but that it should be ignored. + *

    + */ + public void setIgnoreHeader(); + + /** + * Set the frequency with which a INFO message will be logged showing the + * progress of the processing. You might set this to 1000 or 10000 etc. + *

    + * If this is not set then no INFO messages will be logged. + *

    + */ + public void setLogInfoFrequency(int logInfoFrequency); + + /** + * Ignore the next column of data. + */ + public void addIgnore(); + + /** + * Define the property which will be loaded from the next column of data. + *

    + * This takes into account the data type of the property and handles the + * String to object conversion automatically. + *

    + */ + public void addProperty(String propertyName); + + /** + * Define the next property to be a reference. This effectively means it + * represents a foreign key. For example, with an Address object a Country + * Code could be a reference. + */ + public void addReference(String propertyName); + + /** + * Define the next property and use a custom StringParser to convert the + * string content into the appropriate type for the property. + */ + public void addProperty(String propertyName, StringParser parser); + + /** + * Add a property with a custom Date/Time/Timestamp format using the default + * Locale. This will convert the string into the appropriate java type for the + * given property (Date, Calendar, SQL Date, Time, Timestamp, JODA etc). + */ + public void addDateTime(String propertyName, String dateTimeFormat); + + /** + * Add a property with a custom Date/Time/Timestamp format. This will convert + * the string into the appropriate java type for the given property (Date, + * Calendar, SQL Date, Time, Timestamp, JODA etc). + */ + public void addDateTime(String propertyName, String dateTimeFormat, Locale locale); + + /** + * Automatically create a transaction if required to process all the CSV + * content from the reader. + *

    + * This will check for a current transaction. If there is no current + * transaction then one is started and will commit (or rollback) at the end of + * processing. This will also set the persistBatchSize on the transaction. + *

    + */ + public void process(Reader reader) throws Exception; + + /** + * Process the CSV content passing the bean to the CsvCallback after each row. + *

    + * This provides you with the ability to modify and process the bean. + *

    + *

    + * When using a CsvCallback the reader WILL NOT create a transaction or save + * the bean(s) for you. If you want to insert the processed beans you must + * create your own transaction and save the bean(s) yourself. + *

    + */ + public void process(Reader reader, CsvCallback callback) throws Exception; + +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/text/csv/DefaultCsvCallback.java b/src/main/java/com/avaje/ebean/text/csv/DefaultCsvCallback.java new file mode 100644 index 000000000..e0c7c10d0 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/csv/DefaultCsvCallback.java @@ -0,0 +1,204 @@ +package com.avaje.ebean.text.csv; + +import com.avaje.ebean.EbeanServer; +import com.avaje.ebean.Transaction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides the default implementation of CsvCallback. + *

    + * This handles transaction creation (if no current transaction existed) and + * transaction commit or rollback on error. + *

    + *

    + * For customising the processing you can extend this object and override the + * appropriate methods. + *

    + * + * @author rob + * + * @param + */ +public class DefaultCsvCallback implements CsvCallback { + + private static final Logger logger = LoggerFactory.getLogger(DefaultCsvCallback.class); + + /** + * The transaction to use (if not using CsvCallback). + */ + protected Transaction transaction; + + /** + * Flag set when we created the transaction. + */ + protected boolean createdTransaction; + + /** + * The EbeanServer used to save the beans. + */ + protected EbeanServer server; + + /** + * Used to log a message to indicate progress through large files. + */ + protected int logInfoFrequency; + + /** + * The batch size used when saving the beans. + */ + protected int persistBatchSize; + + /** + * The time the process started. + */ + protected long startTime; + + /** + * The execution time of the process. + */ + protected long exeTime; + + /** + * Construct with a default batch size of 30 and logging info messages every + * 1000 rows. + */ + public DefaultCsvCallback() { + this(30, 1000); + } + + /** + * Construct with explicit batch size and logging info frequency. + */ + public DefaultCsvCallback(int persistBatchSize, int logInfoFrequency) { + + this.persistBatchSize = persistBatchSize; + this.logInfoFrequency = logInfoFrequency; + } + + /** + * Create a transaction if required. + */ + public void begin(EbeanServer server) { + this.server = server; + this.startTime = System.currentTimeMillis(); + + initTransactionIfRequired(); + } + + /** + * Override to read the heading line. + *

    + * This is only called if {@link CsvReader#setHasHeader(boolean,boolean)} is + * set to true. + *

    + *

    + * By default this does nothing (effectively ignoring the heading). + *

    + */ + public void readHeader(String[] line) { + + } + + /** + * Validate that the content is valid and return false if the row should be + * ignored. + *

    + * By default this just returns true. + *

    + *

    + * Override this to add custom validation logic returning false if you want + * the row to be ignored. For example, if all the content is empty return + * false to ignore the row (rather than having the processing fail with some + * error). + *

    + */ + public boolean processLine(int row, String[] line) { + return true; + } + + /** + * Will save the bean. + *

    + * Override this method to customise the bean (set additional properties etc) + * or to control the saving of other related beans (when you can't/don't want + * to use Cascade.PERSIST etc). + *

    + */ + public void processBean(int row, String[] line, T bean) { + + // assumes single bean or Cascade.PERSIST will save any + // related beans (e.g. customer -> customer.billingAddress + server.save(bean, transaction); + + if (logInfoFrequency > 0 && (row % logInfoFrequency == 0)) { + logger.info("processed " + row + " rows"); + } + } + + /** + * Commit the transaction if one was created. + */ + public void end(int row) { + + commitTransactionIfCreated(); + + exeTime = System.currentTimeMillis() - startTime; + logger.info("Csv finished, rows[" + row + "] exeMillis[" + exeTime + "]"); + } + + /** + * Rollback the transaction if one was created. + */ + public void endWithError(int row, Exception e) { + rollbackTransactionIfCreated(e); + } + + /** + * Create a transaction if one is not already active and set its batch mode + * and batch size. + */ + protected void initTransactionIfRequired() { + + transaction = server.currentTransaction(); + if (transaction == null || !transaction.isActive()) { + + transaction = server.beginTransaction(); + createdTransaction = true; + if (persistBatchSize > 1) { + logger.info("Creating transaction, batchSize[" + persistBatchSize + "]"); + transaction.setBatchMode(true); + transaction.setBatchSize(persistBatchSize); + + } else { + // explicitly turn off JDBC batching in case + // is has been turned on globally + transaction.setBatchMode(false); + logger.info("Creating transaction with no JDBC batching"); + } + } + } + + /** + * If we created a transaction commit it. We have successfully processed all + * the rows. + */ + protected void commitTransactionIfCreated() { + if (createdTransaction) { + transaction.commit(); + logger.info("Committed transaction"); + } + } + + /** + * Rollback the transaction if we where not successful in processing all the + * rows. + */ + protected void rollbackTransactionIfCreated(Throwable e) { + if (createdTransaction) { + transaction.rollback(e); + logger.info("Rolled back transaction"); + } + } + +} diff --git a/src/main/java/com/avaje/ebean/text/csv/package-info.java b/src/main/java/com/avaje/ebean/text/csv/package-info.java new file mode 100644 index 000000000..3ea1e3d80 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/csv/package-info.java @@ -0,0 +1,4 @@ +/** + * CSV processing objects. + */ +package com.avaje.ebean.text.csv; \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/text/json/JsonContext.java b/src/main/java/com/avaje/ebean/text/json/JsonContext.java new file mode 100644 index 000000000..17dc030ec --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/json/JsonContext.java @@ -0,0 +1,131 @@ +package com.avaje.ebean.text.json; + +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Type; +import java.util.List; + +/** + * Converts objects to and from JSON format. + * + * @author rbygrave + */ +public interface JsonContext { + + /** + * Convert json string input into a Bean of a specific type. + */ + public T toBean(Class rootType, String json); + + /** + * Convert json reader input into a Bean of a specific type. + */ + public T toBean(Class rootType, Reader json); + + /** + * Convert json string input into a Bean of a specific type with options. + */ + public T toBean(Class rootType, String json, JsonReadOptions options); + + /** + * Convert json reader input into a Bean of a specific type with options. + */ + public T toBean(Class rootType, Reader json, JsonReadOptions options); + + /** + * Convert json string input into a list of beans of a specific type. + */ + public List toList(Class rootType, String json); + + /** + * Convert json string input into a list of beans of a specific type with + * options. + */ + public List toList(Class rootType, String json, JsonReadOptions options); + + /** + * Convert json reader input into a list of beans of a specific type. + */ + public List toList(Class rootType, Reader json); + + /** + * Convert json reader input into a list of beans of a specific type with + * options. + */ + public List toList(Class rootType, Reader json, JsonReadOptions options); + + /** + * Use the genericType to determine if this should be converted into a List or + * bean. + */ + public Object toObject(Type genericType, Reader json, JsonReadOptions options); + + /** + * Use the genericType to determine if this should be converted into a List or + * bean. + */ + public Object toObject(Type genericType, String json, JsonReadOptions options); + + /** + * Write the bean or collection in JSON format to the writer with default + * options. + * + * @param o + * the bean or collection of beans to write + * @param writer + * used to write the json output to + */ + public void toJsonWriter(Object o, Writer writer); + + /** + * With additional pretty output option. + */ + public void toJsonWriter(Object o, Writer writer, boolean pretty); + + /** + * With additional options to specify JsonValueAdapter and + * JsonWriteBeanVisitor's. + * + * @param o + * the bean or collection of beans to write + * @param writer + * used to write the json output to + * @param options + * additional options to control the JSON output + */ + public void toJsonWriter(Object o, Writer writer, boolean pretty, JsonWriteOptions options); + + /** + * With additional JSONP callback function. + */ + public void toJsonWriter(Object o, Writer writer, boolean pretty, JsonWriteOptions options, + String callback); + + /** + * Convert a bean or collection to json string using default options. + */ + public String toJsonString(Object o); + + /** + * Convert a bean or collection to json string with pretty format using + * default options. + */ + public String toJsonString(Object o, boolean pretty); + + /** + * Convert a bean or collection to json string using options. + */ + public String toJsonString(Object o, boolean pretty, JsonWriteOptions options); + + /** + * Convert a bean or collection to json string using a JSONP callback. + */ + public String toJsonString(Object o, boolean pretty, JsonWriteOptions options, String callback); + + /** + * Return true if the type is known as an Entity or Xml type or a List Set or + * Map of known bean types. + */ + public boolean isSupportedType(Type genericType); + +} \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/text/json/JsonElement.java b/src/main/java/com/avaje/ebean/text/json/JsonElement.java new file mode 100644 index 000000000..98b76a224 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/json/JsonElement.java @@ -0,0 +1,39 @@ +package com.avaje.ebean.text.json; + +/** + * Marker interface for all the Raw JSON types. + *

    + * You will only use the JsonElements when you register a JsonReadBeanVisitor. + * The JSON elements that are not mapped to a bean property are made available + * to the JsonReadBeanVisitor. + *

    + * + * @see JsonReadBeanVisitor + * + * @author rbygrave + */ +public interface JsonElement { + + /** + * Return true if this is a JSON primitive type (null, boolean, number or + * string). + */ + public boolean isPrimitive(); + + /** + * Return the string value of this primitive JSON element. + *

    + * This can not be used for JsonElementObject or JsonElementArray. + *

    + */ + public String toPrimitiveString(); + + public Object eval(String exp); + + public int evalInt(String exp); + + public String evalString(String exp); + + public boolean evalBoolean(String exp); + +} diff --git a/src/main/java/com/avaje/ebean/text/json/JsonElementArray.java b/src/main/java/com/avaje/ebean/text/json/JsonElementArray.java new file mode 100644 index 000000000..5a59e28bc --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/json/JsonElementArray.java @@ -0,0 +1,112 @@ +package com.avaje.ebean.text.json; + +import java.util.ArrayList; +import java.util.List; + +/** + * JSON Array element. + *

    + * You will only use the JsonElements when you register a JsonReadBeanVisitor. + * The JSON elements that are not mapped to a bean property are made available + * to the JsonReadBeanVisitor. + *

    + * + * @see JsonReadBeanVisitor + * + * @author rbygrave + */ +public class JsonElementArray implements JsonElement { + + private final List values = new ArrayList(); + + public List getValues() { + return values; + } + + public void add(JsonElement value) { + values.add(value); + } + + public String toString() { + return values.toString(); + } + + public boolean isPrimitive() { + return false; + } + + public String toPrimitiveString() { + return null; + } + + private String[] split(String exp) { + int pos = exp.indexOf('.'); + if (pos == -1) { + return new String[] { exp, null }; + } + String exp0 = exp.substring(0, pos); + String exp1 = exp.substring(pos + 1); + return new String[] { exp0, exp1 }; + } + + public Object eval(String exp) { + String[] e = split(exp); + return eval(e[0], e[1]); + } + + public int evalInt(String exp) { + String[] e = split(exp); + return evalInt(e[0], e[1]); + } + + public String evalString(String exp) { + String[] e = split(exp); + return evalString(e[0], e[1]); + } + + public boolean evalBoolean(String exp) { + // TODO Auto-generated method stub + return false; + } + + private Object eval(String exp0, String exp1) { + if ("size".equals(exp0)) { + return values.size(); + } + if ("isEmpty".equals(exp0)) { + return values.isEmpty(); + } + int idx = Integer.parseInt(exp0); + JsonElement element = values.get(idx); + return element.eval(exp1); + } + + private int evalInt(String exp0, String exp1) { + if ("size".equals(exp0)) { + return values.size(); + } + if ("isEmpty".equals(exp0)) { + return values.isEmpty() ? 1 : 0; + } + int idx = Integer.parseInt(exp0); + JsonElement element = values.get(idx); + return element.evalInt(exp1); + } + + private String evalString(String exp0, String exp1) { + if ("size".equals(exp0)) { + return String.valueOf(values.size()); + } + if ("isEmpty".equals(exp0)) { + return String.valueOf(values.isEmpty()); + } + int idx = Integer.parseInt(exp0); + JsonElement element = values.get(idx); + return element.evalString(exp1); + } + + public String getString() { + return toString(); + } + +} diff --git a/src/main/java/com/avaje/ebean/text/json/JsonElementBoolean.java b/src/main/java/com/avaje/ebean/text/json/JsonElementBoolean.java new file mode 100644 index 000000000..69132b8a2 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/json/JsonElementBoolean.java @@ -0,0 +1,63 @@ +package com.avaje.ebean.text.json; + +/** + * JSON boolean element. + *

    + * You will only use the JsonElements when you register a JsonReadBeanVisitor. + * The JSON elements that are not mapped to a bean property are made available + * to the JsonReadBeanVisitor. + *

    + * + * @see JsonReadBeanVisitor + * + * @author rbygrave + */ + +public class JsonElementBoolean implements JsonElement { + + public static final JsonElementBoolean TRUE = new JsonElementBoolean(true); + + public static final JsonElementBoolean FALSE = new JsonElementBoolean(false); + + private final Boolean value; + + private JsonElementBoolean(Boolean value) { + this.value = value; + } + + public Boolean getValue() { + return value; + } + + public String toString() { + return Boolean.toString(value); + } + + public boolean isPrimitive() { + return true; + } + + public String toPrimitiveString() { + return value.toString(); + } + + public Object eval(String exp) { + if (exp != null) { + throw new IllegalArgumentException("expression [" + exp + "] not allowed on boolean"); + } + return value; + } + + public int evalInt(String exp) { + return value ? 1 : 0; + } + + public String evalString(String exp) { + return toString(); + } + + public boolean evalBoolean(String exp) { + return value; + } + +} diff --git a/src/main/java/com/avaje/ebean/text/json/JsonElementNull.java b/src/main/java/com/avaje/ebean/text/json/JsonElementNull.java new file mode 100644 index 000000000..b38406a57 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/json/JsonElementNull.java @@ -0,0 +1,57 @@ +package com.avaje.ebean.text.json; + +/** + * JSON null element. + *

    + * You will only use the JsonElements when you register a JsonReadBeanVisitor. + * The JSON elements that are not mapped to a bean property are made available + * to the JsonReadBeanVisitor. + *

    + * + * @see JsonReadBeanVisitor + * + * @author rbygrave + */ +public class JsonElementNull implements JsonElement { + + public static final JsonElementNull NULL = new JsonElementNull(); + + private JsonElementNull() { + } + + public String getValue() { + return "null"; + } + + public String toString() { + return "json null"; + } + + public boolean isPrimitive() { + return true; + } + + public String toPrimitiveString() { + return null; + } + + public Object eval(String exp) { + if (exp != null) { + throw new IllegalArgumentException("expression [" + exp + "] not allowed on null"); + } + return null; + } + + public int evalInt(String exp) { + return 0; + } + + public String evalString(String exp) { + return null; + } + + public boolean evalBoolean(String exp) { + return false; + } + +} diff --git a/src/main/java/com/avaje/ebean/text/json/JsonElementNumber.java b/src/main/java/com/avaje/ebean/text/json/JsonElementNumber.java new file mode 100644 index 000000000..a778315ac --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/json/JsonElementNumber.java @@ -0,0 +1,67 @@ +package com.avaje.ebean.text.json; + +/** + * JSON number element. + *

    + * You will only use the JsonElements when you register a JsonReadBeanVisitor. + * The JSON elements that are not mapped to a bean property are made available + * to the JsonReadBeanVisitor. + *

    + * + * @see JsonReadBeanVisitor + * + * @author rbygrave + */ +public class JsonElementNumber implements JsonElement { + + private final String value; + + public JsonElementNumber(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public String toString() { + return value; + } + + public boolean isPrimitive() { + return true; + } + + public String toPrimitiveString() { + return value; + } + + public Object eval(String exp) { + if (exp != null) { + throw new IllegalArgumentException("expression [" + exp + "] not allowed on number"); + } + return Double.parseDouble(value); + } + + public int evalInt(String exp) { + if (exp != null) { + throw new IllegalArgumentException("expression [" + exp + "] not allowed on number"); + } + return Integer.parseInt(value); + } + + public String evalString(String exp) { + if (exp != null) { + throw new IllegalArgumentException("expression [" + exp + "] not allowed on number"); + } + return value; + } + + public boolean evalBoolean(String exp) { + if (exp != null) { + throw new IllegalArgumentException("expression [" + exp + "] not allowed on number"); + } + return Boolean.parseBoolean(value); + } + +} diff --git a/src/main/java/com/avaje/ebean/text/json/JsonElementObject.java b/src/main/java/com/avaje/ebean/text/json/JsonElementObject.java new file mode 100644 index 000000000..028158e16 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/json/JsonElementObject.java @@ -0,0 +1,108 @@ +package com.avaje.ebean.text.json; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * JSON Object element. + *

    + * You will only use the JsonElements when you register a JsonReadBeanVisitor. + * The JSON elements that are not mapped to a bean property are made available + * to the JsonReadBeanVisitor. + *

    + * + * @see JsonReadBeanVisitor + * + * @author rbygrave + */ +public class JsonElementObject implements JsonElement { + + private final Map map = new LinkedHashMap(); + + public void put(String key, JsonElement value) { + map.put(key, value); + } + + private String[] split(String exp) { + int pos = exp.indexOf('.'); + if (pos == -1) { + return new String[] { exp, null }; + } + String exp0 = exp.substring(0, pos); + String exp1 = exp.substring(pos + 1); + return new String[] { exp0, exp1 }; + } + + public Object eval(String exp) { + String[] e = split(exp); + return eval(e[0], e[1]); + } + + public int evalInt(String exp) { + String[] e = split(exp); + return evalInt(e[0], e[1]); + } + + public String evalString(String exp) { + if (exp == null) { + return map.toString(); + } + String[] e = split(exp); + return evalString(e[0], e[1]); + } + + public boolean evalBoolean(String exp) { + String[] e = split(exp); + return evalBoolean(e[0], e[1]); + } + + private Object eval(String exp0, String exp1) { + JsonElement e = map.get(exp0); + return e == null ? null : e.eval(exp1); + } + + private int evalInt(String exp0, String exp1) { + JsonElement e = map.get(exp0); + return e == null ? 0 : e.evalInt(exp1); + } + + private String evalString(String exp0, String exp1) { + JsonElement e = map.get(exp0); + return e == null ? "" : e.evalString(exp1); + } + + private boolean evalBoolean(String exp0, String exp1) { + JsonElement e = map.get(exp0); + return e == null ? false : e.evalBoolean(exp1); + } + + public JsonElement get(String key) { + return map.get(key); + } + + public JsonElement getValue(String key) { + return map.get(key); + } + + public Set keySet() { + return map.keySet(); + } + + public Set> entrySet() { + return map.entrySet(); + } + + public String toString() { + return map.toString(); + } + + public boolean isPrimitive() { + return false; + } + + public String toPrimitiveString() { + return null; + } + +} diff --git a/src/main/java/com/avaje/ebean/text/json/JsonElementString.java b/src/main/java/com/avaje/ebean/text/json/JsonElementString.java new file mode 100644 index 000000000..0b76feafc --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/json/JsonElementString.java @@ -0,0 +1,71 @@ +package com.avaje.ebean.text.json; + +/** + * JSON string element. + *

    + * You will only use the JsonElements when you register a JsonReadBeanVisitor. + * The JSON elements that are not mapped to a bean property are made available + * to the JsonReadBeanVisitor. + *

    + * + * @see JsonReadBeanVisitor + * + * @author rbygrave + */ +public class JsonElementString implements JsonElement { + + private final String value; + + public JsonElementString(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public String toString() { + return value; + } + + public boolean isPrimitive() { + return true; + } + + public String toPrimitiveString() { + return value; + } + + public Object eval(String exp) { + if (exp != null) { + throw new IllegalArgumentException("expression [" + exp + "] not allowed on number"); + } + return value; + } + + public int evalInt(String exp) { + if (exp != null) { + throw new IllegalArgumentException("expression [" + exp + "] not allowed on number"); + } + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return 0; + } + } + + public String evalString(String exp) { + if (exp != null) { + throw new IllegalArgumentException("expression [" + exp + "] not allowed on number"); + } + return value; + } + + public boolean evalBoolean(String exp) { + if (exp != null) { + throw new IllegalArgumentException("expression [" + exp + "] not allowed on number"); + } + return Boolean.parseBoolean(exp); + } + +} diff --git a/src/main/java/com/avaje/ebean/text/json/JsonReadBeanVisitor.java b/src/main/java/com/avaje/ebean/text/json/JsonReadBeanVisitor.java new file mode 100644 index 000000000..6de15360c --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/json/JsonReadBeanVisitor.java @@ -0,0 +1,34 @@ +package com.avaje.ebean.text.json; + +import java.util.Map; + +/** + * Provides for some custom handling of json content as it is read. + *

    + * This visit method is called after all the known properties of the bean have + * been processed. Any JSON elements that could not be mapped to known bean + * properties are available in the unmapped Map. + *

    + * + * @author rbygrave + * + * @param + * The type of entity bean + */ +public interface JsonReadBeanVisitor { + + /** + * Visit the bean that has just been processed. + *

    + * This provides a method of customising the bean and processing any custom + * JSON content. + *

    + * + * @param bean + * the bean being processed + * @param unmapped + * Map of any JSON elements that didn't map to known bean properties + */ + public void visit(T bean, Map unmapped); + +} diff --git a/src/main/java/com/avaje/ebean/text/json/JsonReadOptions.java b/src/main/java/com/avaje/ebean/text/json/JsonReadOptions.java new file mode 100644 index 000000000..a6832fd21 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/json/JsonReadOptions.java @@ -0,0 +1,71 @@ +package com.avaje.ebean.text.json; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Provides the ability to customise the reading of JSON content. + *

    + * You can optionally provide a custom JsonValueAdapter to handle specific + * formatting for Date and DateTime types. + *

    + *

    + * You can optionally register JsonReadBeanVisitors to customise the processing + * of the beans as they are processed and handle any custom JSON elements that + * could not be mapped to bean properties. + *

    + * + * @author rbygrave + * + */ +public class JsonReadOptions { + + protected JsonValueAdapter valueAdapter; + + protected Map> visitorMap; + + /** + * Default constructor. + */ + public JsonReadOptions() { + this.visitorMap = new LinkedHashMap>(); + } + + /** + * Return the JsonValueAdapter. + */ + public JsonValueAdapter getValueAdapter() { + return valueAdapter; + } + + /** + * Return the map of JsonReadBeanVisitor's. + */ + public Map> getVisitorMap() { + return visitorMap; + } + + /** + * Set a JsonValueAdapter for custom DateTime and Date formatting. + */ + public JsonReadOptions setValueAdapter(JsonValueAdapter valueAdapter) { + this.valueAdapter = valueAdapter; + return this; + } + + /** + * Register a JsonReadBeanVisitor for the root level. + */ + public JsonReadOptions addRootVisitor(JsonReadBeanVisitor visitor) { + return addVisitor(null, visitor); + } + + /** + * Register a JsonReadBeanVisitor for a given path. + */ + public JsonReadOptions addVisitor(String path, JsonReadBeanVisitor visitor) { + visitorMap.put(path, visitor); + return this; + } + +} diff --git a/src/main/java/com/avaje/ebean/text/json/JsonValueAdapter.java b/src/main/java/com/avaje/ebean/text/json/JsonValueAdapter.java new file mode 100644 index 000000000..a30dcf38c --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/json/JsonValueAdapter.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2009 Authors + * + * This file is part of Ebean. + * + * Ebean is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * Ebean is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Ebean; if not, write to the Free Software Foundation, Inc., + * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.avaje.ebean.text.json; + +import java.sql.Timestamp; + +/** + * Allows you to customise the Date and Timestamp formats. + *

    + * There is not a standard JSON format for Date or Timestamp types. By default + * Ebean uses ISO8601 "yyyy-MM-dd'T'HH:mm:ss.SSSZ" and "yyyy-MM-dd". + *

    + *

    + * Note that Ebean will convert Joda types to either of the Date or Timestamp + * types and back for you. + *

    + * + * @see JsonReadOptions + * + * @author rbygrave + */ +public interface JsonValueAdapter { + + /** + * Convert the Date to json string. + */ + public String jsonFromDate(java.sql.Date date); + + /** + * Convert the DateTime to json string. + */ + public String jsonFromTimestamp(java.sql.Timestamp date); + + /** + * Parse the JSON string into a Date. + */ + public java.sql.Date jsonToDate(String jsonDate); + + /** + * Parse the JSON DateTime into a Timestamp. + */ + public Timestamp jsonToTimestamp(String jsonDateTime); + +} diff --git a/src/main/java/com/avaje/ebean/text/json/JsonWriteBeanVisitor.java b/src/main/java/com/avaje/ebean/text/json/JsonWriteBeanVisitor.java new file mode 100644 index 000000000..eb44cf788 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/json/JsonWriteBeanVisitor.java @@ -0,0 +1,33 @@ +package com.avaje.ebean.text.json; + +/** + * Allows for customising the JSON write processing. + *

    + * You can use this to add raw JSON content via {@link JsonWriter}. + *

    + *

    + * You register a JsonWriteBeanVisitor with {@link JsonWriteOptions}. + *

    + * + * @author rbygrave + * + * @param + * the type of entity bean + * + * @see JsonWriteOptions + */ +public interface JsonWriteBeanVisitor { + + /** + * Visit the bean that has just been writing it's content to JSON. You can + * write your own additional JSON content to the JsonWriter if you wish. + * + * @param bean + * the bean that has been writing it's content + * @param jsonWriter + * the JsonWriter which you can append custom json content to if you + * wish. + */ + public void visit(T bean, JsonWriter jsonWriter); + +} diff --git a/src/main/java/com/avaje/ebean/text/json/JsonWriteOptions.java b/src/main/java/com/avaje/ebean/text/json/JsonWriteOptions.java new file mode 100644 index 000000000..8775f5001 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/json/JsonWriteOptions.java @@ -0,0 +1,237 @@ +package com.avaje.ebean.text.json; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import com.avaje.ebean.text.PathProperties; + +/** + * Provides options for customising the JSON write process. + *

    + * You can optionally provide a custom JsonValueAdapter to handle specific + * formatting for Date and DateTime types. + *

    + *

    + * You can optionally register JsonWriteBeanVisitors to customise the processing + * of the beans as they are processed and add raw JSON + * elements. + *

    + *

    + * You can explicitly state which properties to include in the JSON output for + * the root level and each path. + *

    + * + *
    + * // find some customers ...
    + * 
    + * List<Customer> list = Ebean.find(Customer.class).select("id, name, status, shippingAddress")
    + *     .fetch("billingAddress",
    + *         "line1, city").fetch("billingAddress.country", "*").fetch("contacts", "firstName,email")
    + *     .order().desc("id")
    + *     .findList();
    + * 
    + * JsonContext json = Ebean.createJsonContext();
    + * 
    + * JsonWriteOptions writeOptions = new JsonWriteOptions();
    + * writeOptions.setRootPathVisitor(new JsonWriteBeanVisitor<Customer>() {
    + * 
    + *   public void visit(Customer bean, JsonWriter ctx) {
    + *     System.out.println("write visit customer: " + bean);
    + *     ctx.appendKeyValue("dummyCust", "34");
    + *     ctx.appendKeyValue("smallCustObject", "{\"a\":34,\"b\":\"asdasdasd\"}");
    + *   }
    + * });
    + * 
    + * writeOptions.setPathProperties("contacts", "firstName,id");
    + * writeOptions.setPathVisitor("contacts", new JsonWriteBeanVisitor<Contact>() {
    + * 
    + *   public void visit(Contact bean, JsonWriter ctx) {
    + *     System.out.println("write additional custom json on customer: " + bean);
    + *     ctx.appendKeyValue("dummy", "  3400" + bean.getId() + "");
    + *     ctx.appendKeyValue("smallObject", "{\"contactA\":34,\"contactB\":\"banana\"}");
    + *   }
    + * 
    + * });
    + * 
    + * // output as a JSON string with pretty formatting
    + * String s = json.toJsonString(list, true, writeOptions);
    + * 
    + * 
    + * + * @see JsonContext#toList(Class, String, JsonReadOptions) + * + * @author rbygrave + * + */ +public class JsonWriteOptions { + + protected String callback; + + protected JsonValueAdapter valueAdapter; + + protected Map> visitorMap; + + protected PathProperties pathProperties; + + /** + * Parse and return a PathProperties from nested string format like + * (a,b,c(d,e),f(g)) where "c" is a path containing "d" and "e" and "f" is a + * path containing "g" and the root path contains "a","b","c" and "f". + */ + public static JsonWriteOptions parsePath(String pathProperties) { + + PathProperties p = PathProperties.parse(pathProperties); + JsonWriteOptions o = new JsonWriteOptions(); + o.setPathProperties(p); + return o; + } + + /** + * This creates and returns a copy of these options. + *

    + * Note that it assumes that the JsonWriteBeanVisitor (if defined) are + * immutable and any JsonWriteBeanVisitor instances are shared between the + * original and the copy. + *

    + */ + public JsonWriteOptions copy() { + JsonWriteOptions copy = new JsonWriteOptions(); + copy.callback = callback; + copy.valueAdapter = valueAdapter; + copy.pathProperties = pathProperties; + if (visitorMap != null) { + copy.visitorMap = new HashMap>(visitorMap); + } + return copy; + } + + /** + * Return a JSONP callback function. + */ + public String getCallback() { + return callback; + } + + /** + * Set a JSONP callback function. + */ + public JsonWriteOptions setCallback(String callback) { + this.callback = callback; + return this; + } + + /** + * Return the JsonValueAdapter. + */ + public JsonValueAdapter getValueAdapter() { + return valueAdapter; + } + + /** + * Set a JsonValueAdapter for custom DateTime and Date formatting. + */ + public JsonWriteOptions setValueAdapter(JsonValueAdapter valueAdapter) { + this.valueAdapter = valueAdapter; + return this; + } + + /** + * Register a JsonWriteBeanVisitor for the root level. + */ + public JsonWriteOptions setRootPathVisitor(JsonWriteBeanVisitor visitor) { + return setPathVisitor(null, visitor); + } + + /** + * Register a JsonWriteBeanVisitor for the given path. + */ + public JsonWriteOptions setPathVisitor(String path, JsonWriteBeanVisitor visitor) { + if (visitorMap == null) { + visitorMap = new HashMap>(); + } + visitorMap.put(path, visitor); + return this; + } + + /** + * Set the properties to include in the JSON output for the given path. + * + * @param propertiesToInclude + * The set of properties to output + */ + public JsonWriteOptions setPathProperties(String path, Set propertiesToInclude) { + if (pathProperties == null) { + pathProperties = new PathProperties(); + } + pathProperties.put(path, propertiesToInclude); + return this; + } + + /** + * Set the properties to include in the JSON output for the given path. + * + * @param propertiesToInclude + * Comma delimited list of properties to output + */ + public JsonWriteOptions setPathProperties(String path, String propertiesToInclude) { + return setPathProperties(path, parseProps(propertiesToInclude)); + } + + /** + * Set the properties to include in the JSON output for the root level. + * + * @param propertiesToInclude + * Comma delimited list of properties to output + */ + public JsonWriteOptions setRootPathProperties(String propertiesToInclude) { + return setPathProperties(null, parseProps(propertiesToInclude)); + } + + /** + * Set the properties to include in the JSON output for the root level. + * + * @param propertiesToInclude + * The set of properties to output + */ + public JsonWriteOptions setRootPathProperties(Set propertiesToInclude) { + return setPathProperties(null, propertiesToInclude); + } + + private Set parseProps(String propertiesToInclude) { + + LinkedHashSet props = new LinkedHashSet(); + + String[] split = propertiesToInclude.split(","); + for (int i = 0; i < split.length; i++) { + String s = split[i].trim(); + if (s.length() > 0) { + props.add(s); + } + } + return props; + } + + /** + * Return the Map of registered JsonWriteBeanVisitor's by path. + */ + public Map> getVisitorMap() { + return visitorMap; + } + + /** + * Set the Map of properties to include by path. + */ + public void setPathProperties(PathProperties pathProperties) { + this.pathProperties = pathProperties; + } + + /** + * Return the properties to include by path. + */ + public PathProperties getPathProperties() { + return pathProperties; + } + +} diff --git a/src/main/java/com/avaje/ebean/text/json/JsonWriter.java b/src/main/java/com/avaje/ebean/text/json/JsonWriter.java new file mode 100644 index 000000000..58d8dd754 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/json/JsonWriter.java @@ -0,0 +1,28 @@ +package com.avaje.ebean.text.json; + +/** + * The JSON Writer made available to JsonWriteBeanVisitor's so that you can + * append your own JSON content into the output. + * + * @see JsonWriteBeanVisitor + * @see JsonWriteOptions#setRootPathVisitor(JsonWriteBeanVisitor) + * @see JsonWriteOptions#setPathVisitor(String, JsonWriteBeanVisitor) + * + * @author rbygrave + */ +public interface JsonWriter { + + /** + * Use this to append some custom content into the JSON output. + * + * @param key + * the json key + * + * @param rawJsonValue + * raw json value + */ + public void appendRawValue(String key, String rawJsonValue); + + public void appendQuoteEscapeValue(String key, String rawJsonValue); + +} diff --git a/src/main/java/com/avaje/ebean/text/json/package-info.java b/src/main/java/com/avaje/ebean/text/json/package-info.java new file mode 100644 index 000000000..cb51e51c0 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/json/package-info.java @@ -0,0 +1,50 @@ +/** + * JSON formatting and parsing objects (See JsonContext). + *

    + * The goal is to provide JSON support taking into account + * various ORM issues such as partial objects (for fetching and + * updating), reference beans and + * bi-directional relationships. + *

    + * + *

    Example:

    + *
    + *  // find some customers ...
    + *  
    + * List<Customer> list = Ebean.find(Customer.class)
    + *     .select("id, name, status, shippingAddress")
    + *     .fetch("billingAddress","line1, city")
    + *     .fetch("billingAddress.country", "*")
    + *     .fetch("contacts", "firstName,email")
    + *     .order().desc("id")
    + *     .findList();
    + * 
    + * JsonContext json = Ebean.createJsonContext();
    + * 
    + * JsonWriteOptions writeOptions = new JsonWriteOptions();
    + * writeOptions.setRootPathVisitor(new JsonWriteBeanVisitor<Customer>() {
    + * 
    + *     public void visit(Customer bean, JsonWriter ctx) {
    + *         System.out.println("write visit customer: " + bean);
    + *         ctx.appendKeyValue("dummyCust", "34");
    + *         ctx.appendKeyValue("smallCustObject", "{\"a\":34,\"b\":\"asdasdasd\"}");
    + *     }
    + * });
    + * 
    + * writeOptions.setPathProperties("contacts", "firstName,id");
    + * writeOptions.setPathVisitor("contacts", new JsonWriteBeanVisitor<Contact>() {
    + * 
    + *     public void visit(Contact bean, JsonWriter ctx) {
    + *         System.out.println("write additional custom json on customer: " + bean);
    + *         ctx.appendKeyValue("dummy", "  3400" + bean.getId() + "");
    + *         ctx.appendKeyValue("smallObject", "{\"contactA\":34,\"contactB\":\"banana\"}");
    + *     }
    + * 
    + * });
    + * 
    + *  // output as a JSON string with pretty formatting
    + * String s = json.toJsonString(list, true, writeOptions);
    + * 
    + * 
    + */ +package com.avaje.ebean.text.json; \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/text/package-info.java b/src/main/java/com/avaje/ebean/text/package-info.java new file mode 100644 index 000000000..8573f43f9 --- /dev/null +++ b/src/main/java/com/avaje/ebean/text/package-info.java @@ -0,0 +1,4 @@ +/** + * Utility objects for CSV, JSON and XML processing. + */ +package com.avaje.ebean.text; \ No newline at end of file diff --git a/src/main/java/com/avaje/ebean/util/CamelCaseHelper.java b/src/main/java/com/avaje/ebean/util/CamelCaseHelper.java new file mode 100644 index 000000000..f395b6aa9 --- /dev/null +++ b/src/main/java/com/avaje/ebean/util/CamelCaseHelper.java @@ -0,0 +1,31 @@ +package com.avaje.ebean.util; + +public class CamelCaseHelper { + + /** + * To camel from underscore. + * + * @param underscore + * the underscore + * + * @return the string + */ + public static String toCamelFromUnderscore(String underscore) { + + StringBuffer result = new StringBuffer(); + String[] vals = underscore.split("_"); + + for (int i = 0; i < vals.length; i++) { + String lower = vals[i].toLowerCase(); + if (i > 0) { + char c = Character.toUpperCase(lower.charAt(0)); + result.append(c); + result.append(lower.substring(1)); + } else { + result.append(lower); + } + } + + return result.toString(); + } +} diff --git a/src/main/java/com/avaje/ebean/util/ClassUtil.java b/src/main/java/com/avaje/ebean/util/ClassUtil.java new file mode 100644 index 000000000..f823a539a --- /dev/null +++ b/src/main/java/com/avaje/ebean/util/ClassUtil.java @@ -0,0 +1,23 @@ +package com.avaje.ebean.util; + +/** + * Helper to find classes taking into account the context class loader. + * + * @author rbygrave + */ +public class ClassUtil { + + /** + * Return a new instance of the class using the default constructor. + */ + public static Object newInstance(String className) { + try { + Class cls = Class.forName(className); + return cls.newInstance(); + } catch (Exception e) { + String msg = "Error constructing " + className; + throw new IllegalArgumentException(msg, e); + } + } + +} diff --git a/src/main/java/com/avaje/ebean/util/StringHelper.java b/src/main/java/com/avaje/ebean/util/StringHelper.java new file mode 100644 index 000000000..e7cdf1b97 --- /dev/null +++ b/src/main/java/com/avaje/ebean/util/StringHelper.java @@ -0,0 +1,600 @@ +package com.avaje.ebean.util; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** + * Utility String class that supports String manipulation functions. + */ +public class StringHelper { + + private static final char SINGLE_QUOTE = '\''; + + private static final char DOUBLE_QUOTE = '"'; + + /** + * parses a String of the form name1='value1' name2='value2'. Note that you + * can use either single or double quotes for any particular name value pair + * and the end quote must match the begin quote. + */ + public static HashMap parseNameQuotedValue(String tag) throws RuntimeException { + + if (tag == null || tag.length() < 1) { + return null; + } + + // make sure that the quotes are matched... + // int remainer = countOccurances(tag, ""+quote) % 2; + // if (remainer == 1) { + // dp("remainder = "+remainer); + // throw new StringParsingException("Unmatched quote in "+tag); + // } + + // make sure that th last character is not an equals... + // (check now so I don't need to check this every time..) + if (tag.charAt(tag.length() - 1) == '=') { + throw new RuntimeException("missing quoted value at the end of " + tag); + } + + HashMap map = new HashMap(); + // recursively parse out the name value pairs... + return parseNameQuotedValue(map, tag, 0); + } + + /** + * recursively parse out name value pairs (where the value is quoted, with + * either single or double quotes). + */ + private static HashMap parseNameQuotedValue(HashMap map, + String tag, int pos) throws RuntimeException { + + int equalsPos = tag.indexOf("=", pos); + if (equalsPos > -1) { + // check for begin quote... + char firstQuote = tag.charAt(equalsPos + 1); + if (firstQuote != SINGLE_QUOTE && firstQuote != DOUBLE_QUOTE) { + throw new RuntimeException("missing begin quote at " + (equalsPos) + "[" + + tag.charAt(equalsPos + 1) + "] in [" + tag + "]"); + } + + // check for end quote... + int endQuotePos = tag.indexOf(firstQuote, equalsPos + 2); + if (endQuotePos == -1) { + throw new RuntimeException("missing end quote [" + firstQuote + "] after " + pos + + " in [" + tag + "]"); + } + + // we have a valid name and value... + // dp("pos="+pos+" equalsPos="+equalsPos+" + // endQuotePos="+endQuotePos); + String name = tag.substring(pos, equalsPos); + String value = tag.substring(equalsPos + 2, endQuotePos); + // dp("name="+name+"; value="+value+";"); + + // trim off any whitespace from the front of name... + name = trimFront(name, " "); + if ((name.indexOf(SINGLE_QUOTE) > -1) || (name.indexOf(DOUBLE_QUOTE) > -1)) { + throw new RuntimeException("attribute name contains a quote [" + name + "]"); + } + map.put(name, value); + + return parseNameQuotedValue(map, tag, endQuotePos + 1); + + } else { + // no more equals... stop parsing... + return map; + } + } + + /** + * Returns the number of times a particular String occurs in another String. + * e.g. count the number of single quotes. + */ + public static int countOccurances(String content, String occurs) { + return countOccurances(content, occurs, 0, 0); + } + + private static int countOccurances(String content, String occurs, int pos, int countSoFar) { + int equalsPos = content.indexOf(occurs, pos); + if (equalsPos > -1) { + countSoFar = countSoFar + 1; + pos = equalsPos + occurs.length(); + // dp("countSoFar="+countSoFar+" pos="+pos); + return countOccurances(content, occurs, pos, countSoFar); + } else { + return countSoFar; + } + } + + /** + * Parses out a list of Name Value pairs that are delimited together. Will + * always return a StringMap. If allNameValuePairs is null, or no name values + * can be parsed out an empty StringMap is returned. + * + * @param allNameValuePairs + * the entire string to be parsed. + * @param listDelimiter + * (typically ';') the delimited between the list + * @param nameValueSeparator + * (typically '=') the separator between the name and value + */ + public static Map delimitedToMap(String allNameValuePairs, + String listDelimiter, String nameValueSeparator) { + + HashMap params = new HashMap(); + if ((allNameValuePairs == null) || (allNameValuePairs.length() == 0)) { + return params; + } + // trim off any leading listDelimiter... + allNameValuePairs = trimFront(allNameValuePairs, listDelimiter); + return getKeyValue(params, 0, allNameValuePairs, listDelimiter, nameValueSeparator); + } + + /** + * Trims off recurring strings from the front of a string. + * + * @param source + * the source string + * @param trim + * the string to trim off the front + */ + public static String trimFront(String source, String trim) { + if (source == null) { + return null; + } + if (source.indexOf(trim) == 0) { + // dp("trim ..."); + return trimFront(source.substring(trim.length()), trim); + } else { + return source; + } + } + + /** + * Return true if the value is null or an empty string. + */ + public static boolean isNull(String value) { + if (value == null || value.trim().length() == 0) { + return true; + } + return false; + } + + /** + * Recursively pulls out the key value pairs from a raw string. + */ + private static HashMap getKeyValue(HashMap map, int pos, + String allNameValuePairs, String listDelimiter, String nameValueSeparator) { + + if (pos >= allNameValuePairs.length()) { + // dp("end as "+pos+" >= "+allNameValuePairs.length() ); + return map; + } + + int equalsPos = allNameValuePairs.indexOf(nameValueSeparator, pos); + int delimPos = allNameValuePairs.indexOf(listDelimiter, pos); + + if (delimPos == -1) { + delimPos = allNameValuePairs.length(); + } + if (equalsPos == -1) { + // dp("no more equals..."); + return map; + } + if (delimPos == (equalsPos + 1)) { + // dp("Ignoring as nothing between delim and equals... + // delim:"+delimPos+" eq:"+equalsPos); + return getKeyValue(map, delimPos + 1, allNameValuePairs, listDelimiter, + nameValueSeparator); + } + if (equalsPos > delimPos) { + // there is a key without a value? + String key = allNameValuePairs.substring(pos, delimPos); + key = key.trim(); + if (key.length() > 0) { + map.put(key, null); + } + return getKeyValue(map, delimPos + 1, allNameValuePairs, listDelimiter, + nameValueSeparator); + + } + String key = allNameValuePairs.substring(pos, equalsPos); + + if (delimPos > -1) { + String value = allNameValuePairs.substring(equalsPos + 1, delimPos); + // dp("cont "+key+","+value+" pos:"+pos+" + // len:"+allNameValuePairs.length()); + key = key.trim(); + + map.put(key, value); + pos = delimPos + 1; + + // recurse the rest of the values... + return getKeyValue(map, pos, allNameValuePairs, listDelimiter, nameValueSeparator); + } else { + // dp("ERROR: delimPos < 0 ???"); + return map; + } + } + + /** + * Convert a string that has delimited values (say comma delimited) in a + * String[]. You must explicitly choose whether or not to include empty values + * (say two commas that a right beside each other. + * + *

    + * e.g. "alpha,beta,,theta"
    + * With keepEmpties true, this results in a String[] of size 4 with the third + * one having a String of 0 length. With keepEmpties false, this results in a + * String[] of size 3. + *

    + *

    + *

    + *

    + * e.g. ",alpha,beta,,theta,"
    + * With keepEmpties true, this results in a String[] of size 6 with the + * 1st,4th and 6th one having a String of 0 length. With keepEmpties false, + * this results in a String[] of size 3. + *

    + */ + public static String[] delimitedToArray(String str, String delimiter, boolean keepEmpties) { + + ArrayList list = new ArrayList(); + int startPos = 0; + delimiter(str, delimiter, keepEmpties, startPos, list); + String[] result = new String[list.size()]; + return (String[]) list.toArray(result); + } + + private static void delimiter(String str, String delimiter, boolean keepEmpties, int startPos, + ArrayList list) { + + int endPos = str.indexOf(delimiter, startPos); + if (endPos == -1) { + if (startPos <= str.length()) { + String lastValue = str.substring(startPos, str.length()); + // dp("lastValue="+lastValue); + if (!keepEmpties && lastValue.length() == 0) { + // dp("not keeping..."); + } else { + list.add(lastValue); + } + } + // we have finished parsing the string... + return; + } else { + // get the delimited value... add it.. + String value = str.substring(startPos, endPos); + // dp(startPos+","+endPos+" value="+value); + if (!keepEmpties && value.length() == 0) { + // dp("not keeping..."); + } else { + list.add(value); + } + // recursively search as we are not at the end yet... + delimiter(str, delimiter, keepEmpties, endPos + 1, list); + } + } + + /** + * This returns the FIRST string in str that is bounded on the left by + * leftBound, and bounded on the right by rightBound. This will return null if + * the leftBound is not found within str. + * + *

    + * If leftBound can't be found this returns null. + *

    + *

    + * This rightBound can't be found then this throws a + * StringIndexOutOfBoundsException. + *

    + * + * @param str + * the base string that we will search for the bounded string. + * @param leftBound + * the left bound of the string. + * @param rightBound + * the right bound of the string. + */ + public static String getBoundedString(String str, String leftBound, String rightBound) + throws RuntimeException { + + if (str == null) { + throw new RuntimeException("string to parse is null?"); + } + int startPos = str.indexOf(leftBound); + if (startPos > -1) { + startPos = startPos + leftBound.length(); + int endPos = str.indexOf(rightBound, startPos); + // dp(str+" start:"+startPos+" end:"+endPos); + if (endPos == -1) { + throw new RuntimeException("Can't find rightBound: " + rightBound); + } + return str.substring(startPos, endPos); + } else { + // if no leftBound can be found.. return null... could be in a + // search n parse type loop? + // this keeps "no tag"==null different from "tag not formed + // properly"==StringParsingException + return null; + } + } + + /** + * Takes the String bounded by leftBound & rightBound, and replaces it with + * replaceString. Actually removes the left and right bound strings aswell. + */ + public static String setBoundedString(String str, String leftBound, String rightBound, + String replaceString) { + + int startPos = str.indexOf(leftBound); + if (startPos > -1) { + // startPos = startPos; + int endPos = str.indexOf(rightBound, startPos + leftBound.length()); + if (endPos > -1) { + String toReplace = str.substring(startPos, endPos + 1); + return replaceString(str, toReplace, replaceString); + } else { + return str; + } + } else { + return str; + } + } + + // public static String replaceString(String str, String oldSub, String + // newSub) { + // + // if (str == null) { + // return null; + // } + // StringBuilder newSB = new StringBuilder(str.length()+20); + // int iPos = 0; + // int iPrevPos = 0; + // + // while (true) { + // iPos = str.indexOf(oldSub, iPrevPos); + // if (iPos > -1) { + // // found + // newSB.append(str.substring(iPrevPos, iPos)); + // newSB.append(newSub); + // iPrevPos = iPos + oldSub.length(); + // } else { + // // not found + // newSB.append(str.substring(iPrevPos)); + // break; + // } + // } + // + // return newSB.toString(); + // } + + /** + * This method takes a String and will replace all occurrences of the match + * String with that of the replace String. + * + * @param source + * the source string + * @param match + * the string used to find a match + * @param replace + * the string used to replace match with + * @return the source string after the search and replace + */ + public static String replaceString(String source, String match, String replace) { + if (source == null) { + return null; + } + if (replace == null) { + return source; + } + if (match == null) { + throw new NullPointerException("match is null?"); + } + if (match.equals(replace)) { + return source; + } + return replaceString(source, match, replace, 30, 0, source.length()); + } + + /** + * Additionally specify the additionalSize to add to the buffer. This will + * make the buffer bigger so that it doesn't have to grow when replacement + * occurs. + */ + public static String replaceString(String source, String match, String replace, + int additionalSize, int startPos, int endPos) { + + if (source == null) { + return source; + } + + char match0 = match.charAt(0); + + int matchLength = match.length(); + + if (matchLength == 1 && replace.length() == 1) { + char replace0 = replace.charAt(0); + return source.replace(match0, replace0); + } + if (matchLength >= replace.length()) { + additionalSize = 0; + } + + int sourceLength = source.length(); + int lastMatch = endPos - matchLength; + + StringBuilder sb = new StringBuilder(sourceLength + additionalSize); + + if (startPos > 0) { + sb.append(source.substring(0, startPos)); + } + + char sourceChar; + boolean isMatch; + int sourceMatchPos; + + for (int i = startPos; i < sourceLength; i++) { + sourceChar = source.charAt(i); + if (i > lastMatch || sourceChar != match0) { + sb.append(sourceChar); + + } else { + // check to see if this is a match + isMatch = true; + sourceMatchPos = i; + + // check each following character... + for (int j = 1; j < matchLength; j++) { + sourceMatchPos++; + if (source.charAt(sourceMatchPos) != match.charAt(j)) { + isMatch = false; + break; + } + } + if (isMatch) { + i = i + matchLength - 1; + sb.append(replace); + } else { + // was not a match + sb.append(sourceChar); + } + } + } + + return sb.toString(); + } + + /** + * A search and replace with multiple matching strings. + *

    + * Useful when converting CRNL CR and NL all to a BR tag for example. + *

    + * + *
    +   * 
    +   * String[] multi = { "\r\n", "\r", "\n" };
    +   * content = StringHelper.replaceStringMulti(content, multi, "<br/>");
    +   * 
    +   * 
    + */ + public static String replaceStringMulti(String source, String[] match, String replace) { + return replaceStringMulti(source, match, replace, 30, 0, source.length()); + } + + /** + * Additionally specify an additional size estimate for the buffer plus start + * and end positions. + *

    + * The start and end positions can limit the search and replace. Otherwise + * these default to startPos = 0 and endPos = source.length(). + *

    + */ + public static String replaceStringMulti(String source, String[] match, String replace, + int additionalSize, int startPos, int endPos) { + + int shortestMatch = match[0].length(); + + char[] match0 = new char[match.length]; + for (int i = 0; i < match0.length; i++) { + match0[i] = match[i].charAt(0); + if (match[i].length() < shortestMatch) { + shortestMatch = match[i].length(); + } + } + + StringBuilder sb = new StringBuilder(source.length() + additionalSize); + + char sourceChar; + + int len = source.length(); + int lastMatch = endPos - shortestMatch; + + if (startPos > 0) { + sb.append(source.substring(0, startPos)); + } + + int matchCount = 0; + + for (int i = startPos; i < len; i++) { + sourceChar = source.charAt(i); + if (i > lastMatch) { + sb.append(sourceChar); + } else { + matchCount = 0; + for (int k = 0; k < match0.length; k++) { + if (matchCount == 0 && sourceChar == match0[k]) { + if (match[k].length() + i <= len) { + + ++matchCount; + int j = 1; + for (; j < match[k].length(); j++) { + if (source.charAt(i + j) != match[k].charAt(j)) { + --matchCount; + break; + } + } + if (matchCount > 0) { + i = i + j - 1; + sb.append(replace); + break; + } + } + } + } + if (matchCount == 0) { + sb.append(sourceChar); + } + } + } + + return sb.toString(); + } + + /** + * This method takes a String as an argument and removes all occurrences of + * the supplied Char. It returns the resulting String. + */ + public static String removeChar(String s, char chr) { + + StringBuilder sb = new StringBuilder(s.length()); + + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c != chr) { + sb.append(c); + } + } + + return sb.toString(); + } + + /** + * This method takes a String as an argument and removes all occurrences of + * the supplied Chars. It returns the resulting String. + */ + public static String removeChars(String s, char[] chr) { + + StringBuilder sb = new StringBuilder(s.length()); + + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (!charMatch(c, chr)) { + sb.append(c); + } + } + + return sb.toString(); + } + + private static boolean charMatch(int iChr, char[] chr) { + for (int i = 0; i < chr.length; i++) { + if (iChr == chr[i]) { + return true; + } + } + return false; + } + +}