diff --git a/pom.xml b/pom.xml index 069d6ca..09d094a 100644 --- a/pom.xml +++ b/pom.xml @@ -43,15 +43,10 @@ - - com.samskivert - samskivert - 1.8.3 - com.google.guava guava - 17.0 + 18.0 net.sf.ehcache @@ -65,6 +60,7 @@ 1.7.1 provided + junit junit @@ -77,6 +73,12 @@ 2.2.9 test + + com.samskivert + samskivert + 1.8.2 + test + @@ -130,7 +132,6 @@ public com.samskivert.depot.impl - http://samskivert.github.com/samskivert/apidocs/ http://docs.guava-libraries.googlecode.com/git/javadoc/ http://ehcache.org/apidocs/ diff --git a/src/main/java/com/samskivert/depot/ConnectionProvider.java b/src/main/java/com/samskivert/depot/ConnectionProvider.java new file mode 100644 index 0000000..7babb81 --- /dev/null +++ b/src/main/java/com/samskivert/depot/ConnectionProvider.java @@ -0,0 +1,97 @@ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2012 Michael Bayne, et al. +// http://github.com/samskivert/samskivert/blob/master/COPYING + +package com.samskivert.depot; + +import java.sql.*; + +/** + * As the repository aims to interface with whatever database pooling services a project cares to + * use, it obtains all of its database connections through a connection provider. The connection + * provider provides connections based on a database identifier (a string) which identifies a + * particular connection to a particular database. A user of these services would then coordinate + * the database identifier (or identifiers) used to invoke a database operation with the database + * connections that are returned by the connection provider that they provided to the repository at + * construct time. + */ +public interface ConnectionProvider +{ + /** + * Obtains a database connection based on the supplied database identifier. The repository + * expects to have exclusive use of this connection instance until it releases it. This + * connection will be released subsequently with a call to {@link #releaseConnection} or {@link + * #connectionFailed} depending on the circumstances of the release. close() + * will not be called on the connection. It is up to the connection provider to close + * the connection when it is released if appropriate. + * + * @param ident the database connection identifier. + * @param readOnly whether or not the connection may be to a read-only mirror of the + * repository. + * + * @return an active JDBC connection (which may have come from a connection pool). + * + * @exception DatabaseException thrown if a problem occurs trying to open the requested + * connection. + */ + Connection getConnection (String ident, boolean readOnly); + + /** + * Releases a database connection when it is no longer needed by the repository. + * + * @param ident the database identifier used when obtaining this connection. + * @param readOnly the same value that was passed to {@link #getConnection} to obtain this + * connection. + * @param conn the connection to release (back into the pool or to be closed if pooling is not + * going on). + */ + void releaseConnection (String ident, boolean readOnly, Connection conn); + + /** + * Called by the repository if a failure occurred on the connection. It is expected that the + * connection will be disposed of and subsequent calls to getConnection will + * return a freshly established connection to the database. + * + * @param ident the database identifier used when obtaining this connection. + * @param readOnly the same value that was passed to {@link #getConnection} to obtain this + * connection. + * @param conn the connection that failed. + * @param error the error thrown by the connection (which may be used to determine if the + * connection should be closed or can be reused). + */ + void connectionFailed (String ident, boolean readOnly, Connection conn, SQLException error); + + /** + * Returns a connection that can be used in a transaction. See {@link #getConnection}. Note: + * transaction connections are never read-only. What would be the point? + * + * @exception DatabaseException thrown if a problem occurs trying to open the requested + * connection. + */ + Connection getTxConnection (String ident); + + /** + * Releases a connection obtained by {@link #getTxConnection}. See {@link #releaseConnection}. + */ + void releaseTxConnection (String ident, Connection conn); + + /** + * Reports failure of a connection obtained by {@link #getTxConnection}. See {@link + * #connectionFailed}. + */ + void txConnectionFailed (String ident, Connection conn, SQLException error); + + /** + * Returns the URL associated with this database identifier. This should be the same value that + * would be used if {@link #getConnection} were called. + */ + String getURL (String ident); + + /** + * Shuts down this connection provider, closing all connections currently in the pool. This + * should only be called once all active connections have been released with {@link + * #releaseConnection}. + */ + void shutdown (); +} diff --git a/src/main/java/com/samskivert/depot/DataSourceConnectionProvider.java b/src/main/java/com/samskivert/depot/DataSourceConnectionProvider.java new file mode 100644 index 0000000..bf469ef --- /dev/null +++ b/src/main/java/com/samskivert/depot/DataSourceConnectionProvider.java @@ -0,0 +1,120 @@ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2012 Michael Bayne, et al. +// http://github.com/samskivert/samskivert/blob/master/COPYING + +package com.samskivert.depot; + +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; + +import static com.samskivert.depot.Log.log; + +/** + * Provides connections using a pair of {@link DataSource} instances (one for read-only operations + * and one for read-write operations). Note: if transactions are going to be used, the data sources + * must be pooled data sources, otherwise bad things will happen. + */ +public class DataSourceConnectionProvider implements ConnectionProvider +{ + /** + * Creates a connection provider that will obtain connections from the supplied read-only and + * read-write sources. The ident mechanism is not used by this provider. + * Responsibility for the lifecycle of the supplied datasources is left to the caller and is + * not managed by the connection provider ({@link #shutdown} does nothing). + * + * @param url a URL prefix that can be used by the {@link DatabaseLiaison} to identify the type + * of database being accessed by the supplied data sources, this should be one of "jdbc:mysql" + * or "jdbc:postgresql" as those are the only two types of database currently supported. + */ + public DataSourceConnectionProvider (String url, DataSource readSource, DataSource writeSource) + { + _url = url; + _readSource = readSource; + _writeSource = writeSource; + } + + // from ConnectionProvider + public Connection getConnection (String ident, boolean readOnly) + { + return getConnection(ident, readOnly, null); + } + + // from ConnectionProvider + public void releaseConnection (String ident, boolean readOnly, Connection conn) + { + try { + conn.close(); + } catch (Exception e) { + log.warning("Failure closing connection", + "ident", ident, "ro", readOnly, "conn", conn, e); + } + } + + // from ConnectionProvider + public void connectionFailed (String ident, boolean readOnly, Connection conn, + SQLException error) + { + try { + conn.close(); + } catch (Exception e) { + log.warning("Failure closing failed connection", + "ident", ident, "ro", readOnly, "conn", conn, e); + } + } + + // from ConnectionProvider + public Connection getTxConnection (String ident) + { + // our connections are pooled, so we can just get them normally + return getConnection(ident, false, false); + } + + // from ConnectionProvider + public void releaseTxConnection (String ident, Connection conn) + { + // our connections are pooled, so we can just release them normally + releaseConnection(ident, false, conn); + } + + // from ConnectionProvider + public void txConnectionFailed (String ident, Connection conn, SQLException error) + { + // our connections are pooled, so we can just fail them normally + connectionFailed(ident, false, conn, error); + } + + // from ConnectionProvider + public String getURL (String ident) + { + return _url; + } + + // from ConnectionProvider + public void shutdown () + { + // nothing doing, the caller has to shutdown the datasources + } + + protected Connection getConnection (String ident, boolean readOnly, Boolean autoCommit) + { + try { + Connection conn; + if (readOnly) { + conn = _readSource.getConnection(); + conn.setReadOnly(true); + } else { + conn = _writeSource.getConnection(); + } + if (autoCommit != null) conn.setAutoCommit(autoCommit); + return conn; + + } catch (SQLException sqe) { + throw new DatabaseException(sqe); + } + } + + protected String _url; + protected DataSource _readSource, _writeSource; +} diff --git a/src/main/java/com/samskivert/depot/DepotRepository.java b/src/main/java/com/samskivert/depot/DepotRepository.java index 642ae9e..e5f95fb 100644 --- a/src/main/java/com/samskivert/depot/DepotRepository.java +++ b/src/main/java/com/samskivert/depot/DepotRepository.java @@ -19,13 +19,10 @@ import java.util.Set; import com.google.common.base.Function; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import com.google.common.collect.ObjectArrays; import com.google.common.collect.Sets; import static com.google.common.base.Preconditions.checkArgument; -import com.samskivert.jdbc.ConnectionProvider; -import com.samskivert.jdbc.DatabaseLiaison; -import com.samskivert.util.ArrayUtil; - import com.samskivert.depot.clause.InsertClause; import com.samskivert.depot.clause.Limit; import com.samskivert.depot.clause.QueryClause; @@ -48,6 +45,7 @@ import com.samskivert.depot.impl.SQLBuilder; import com.samskivert.depot.impl.clause.DeleteClause; import com.samskivert.depot.impl.clause.UpdateClause; import com.samskivert.depot.impl.expression.ValueExp; +import com.samskivert.depot.impl.jdbc.DatabaseLiaison; import com.samskivert.depot.impl.util.SeqImpl; /** @@ -143,7 +141,7 @@ public abstract class DepotRepository Key key, CacheStrategy strategy, QueryClause... clauses) throws DatabaseException { - clauses = ArrayUtil.append(clauses, key); + clauses = ObjectArrays.concat(clauses, key); return _ctx.invoke(new FindOneQuery(_ctx, key.getPersistentClass(), strategy, clauses)); } diff --git a/src/main/java/com/samskivert/depot/impl/DropTableMigration.java b/src/main/java/com/samskivert/depot/DropTableMigration.java similarity index 75% rename from src/main/java/com/samskivert/depot/impl/DropTableMigration.java rename to src/main/java/com/samskivert/depot/DropTableMigration.java index dabeb96..586c57a 100644 --- a/src/main/java/com/samskivert/depot/impl/DropTableMigration.java +++ b/src/main/java/com/samskivert/depot/DropTableMigration.java @@ -2,7 +2,7 @@ // Depot library - a Java relational persistence library // https://github.com/threerings/depot/blob/master/LICENSE -package com.samskivert.depot.impl; +package com.samskivert.depot; import java.sql.Connection; import java.sql.SQLException; @@ -10,8 +10,9 @@ import java.sql.SQLException; import com.samskivert.depot.DataMigration; import com.samskivert.depot.DatabaseException; import com.samskivert.depot.PersistenceContext; - -import com.samskivert.jdbc.DatabaseLiaison; +import com.samskivert.depot.impl.DepotMetaData; +import com.samskivert.depot.impl.Modifier; +import com.samskivert.depot.impl.jdbc.DatabaseLiaison; /** * Migration to drop a table. @@ -47,16 +48,7 @@ public class DropTableMigration extends DataMigration } }); // delete the schema version - _ctx.invoke(new Modifier.Simple() { - @Override protected String createQuery (DatabaseLiaison liaison) - { - return "delete from " + - liaison.tableSQL(DepotMetaData.SCHEMA_VERSION_TABLE) + - " where " + - liaison.columnSQL(DepotMetaData.P_COLUMN) + - " = '" + _table + "'"; - } - }); + _ctx.getMetaData().clearVersion(_table); } /** Our persistence context. */ diff --git a/src/main/java/com/samskivert/depot/EHCacheAdapter.java b/src/main/java/com/samskivert/depot/EHCacheAdapter.java index ee6838c..290cfae 100644 --- a/src/main/java/com/samskivert/depot/EHCacheAdapter.java +++ b/src/main/java/com/samskivert/depot/EHCacheAdapter.java @@ -13,7 +13,6 @@ import java.util.concurrent.ConcurrentHashMap; import com.google.common.collect.Maps; import com.google.common.collect.Sets; -import com.samskivert.util.Histogram; import net.sf.ehcache.Cache; import net.sf.ehcache.CacheManager; @@ -43,10 +42,10 @@ public class EHCacheAdapter { public static class EHCachePerformance { - public Histogram lookups; - public Histogram stores; - public Histogram removes; - public Histogram enumerations; + public Stats.Histogram lookups; + public Stats.Histogram stores; + public Stats.Histogram removes; + public Stats.Histogram enumerations; } public static class EHCacheConfig @@ -412,10 +411,10 @@ public class EHCacheAdapter }; protected CacheManager _cachemgr; - protected Histogram _lookups = new Histogram(0, 50, 20); - protected Histogram _stores = new Histogram(0, 50, 20); - protected Histogram _removes = new Histogram(0, 50, 20); - protected Histogram _enumerations = new Histogram(0, 50, 20); + protected Stats.Histogram _lookups = new Stats.Histogram(0, 50, 20); + protected Stats.Histogram _stores = new Stats.Histogram(0, 50, 20); + protected Stats.Histogram _removes = new Stats.Histogram(0, 50, 20); + protected Stats.Histogram _enumerations = new Stats.Histogram(0, 50, 20); protected Map _categories = Collections.synchronizedMap(Maps.newHashMap()); diff --git a/src/main/java/com/samskivert/depot/Key.java b/src/main/java/com/samskivert/depot/Key.java index a6c8ed1..2d3554d 100644 --- a/src/main/java/com/samskivert/depot/Key.java +++ b/src/main/java/com/samskivert/depot/Key.java @@ -10,10 +10,9 @@ import java.util.Collection; import java.util.Map; import com.google.common.base.Function; +import com.google.common.base.Joiner; import com.google.common.collect.Maps; -import com.samskivert.util.StringUtil; - import com.samskivert.depot.clause.WhereClause; import com.samskivert.depot.expression.ColumnExp; import com.samskivert.depot.expression.SQLExpression; @@ -260,7 +259,7 @@ public class Key extends WhereClause // finally make sure we were not given any fields that are not primary key fields checkArgument(map.isEmpty(), "Non-key columns given: " + - StringUtil.join(map.keySet().toArray())); + Joiner.on(", ").join(map.keySet())); return cvalues; } diff --git a/src/main/java/com/samskivert/depot/KeySet.java b/src/main/java/com/samskivert/depot/KeySet.java index 8923777..f80c85d 100644 --- a/src/main/java/com/samskivert/depot/KeySet.java +++ b/src/main/java/com/samskivert/depot/KeySet.java @@ -14,9 +14,6 @@ import java.util.Iterator; import com.google.common.collect.Iterators; import com.google.common.base.Function; -import com.samskivert.util.Logger; -import com.samskivert.util.StringUtil; - import com.samskivert.depot.clause.WhereClause; import com.samskivert.depot.expression.ColumnExp; import com.samskivert.depot.expression.SQLExpression; @@ -167,7 +164,7 @@ public abstract class KeySet extends WhereClause } @Override public String toString () { - return DepotUtil.justClassName(_pClass) + StringUtil.toString(_keys); + return DepotUtil.justClassName(_pClass) + Arrays.toString(_keys); } protected Comparable[] _keys; @@ -216,7 +213,7 @@ public abstract class KeySet extends WhereClause public void validateFlushType (Class pClass) { if (!pClass.equals(_pClass)) { - throw new IllegalArgumentException(Logger.format( + throw new IllegalArgumentException(Log.format( "Class mismatch between persistent record and cache invalidator", "record", pClass.getSimpleName(), "invtype", _pClass.getSimpleName())); } diff --git a/src/main/java/com/samskivert/depot/Log.java b/src/main/java/com/samskivert/depot/Log.java index 81571a2..1dbf828 100644 --- a/src/main/java/com/samskivert/depot/Log.java +++ b/src/main/java/com/samskivert/depot/Log.java @@ -4,13 +4,50 @@ package com.samskivert.depot; -import com.samskivert.util.Logger; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Contains a reference to the log object used by this package. */ public class Log { + public static interface Target { + void debug (String message, Object... args); + void info (String message, Object... args); + void warning (String message, Object... args); + } + /** We dispatch our log messages through this logger. */ - public static Logger log = Logger.getLogger("com.samskivert.depot"); + public static Target log = new Target() { + public void debug (String message, Object... args) { + log(Level.FINE, message, args); + } + public void info (String message, Object... args) { + log(Level.INFO, message, args); + } + public void warning (String message, Object... args) { + log(Level.WARNING, message, args); + } + private void log (Level level, String message, Object[] args) { + Object exn = (args.length == 0) ? null : args[args.length-1]; + if (args.length % 2 == 1 && (exn instanceof Throwable)) { + impl.log(level, format(message, args), (Throwable)exn); + } else { + impl.log(level, format(message, args)); + } + } + private Logger impl = Logger.getLogger("com.samskivert.depot"); + }; + + /** Formats the supplied log message and arguments. */ + public static String format (String message, Object... args) { + if (args.length < 2) return message; + StringBuilder sb = new StringBuilder(message); + sb.append(" ["); + for (int ii = 0; ii < args.length; ii += 2) { + sb.append(args[ii]).append("=").append(args[ii+1]); + } + return sb.append("]").toString(); + } } diff --git a/src/main/java/com/samskivert/depot/MultiKeySet.java b/src/main/java/com/samskivert/depot/MultiKeySet.java index 5047868..cf87259 100644 --- a/src/main/java/com/samskivert/depot/MultiKeySet.java +++ b/src/main/java/com/samskivert/depot/MultiKeySet.java @@ -2,9 +2,6 @@ // Depot library - a Java relational persistence library // https://github.com/threerings/depot/blob/master/LICENSE -/** - * - */ package com.samskivert.depot; import java.util.Arrays; @@ -22,8 +19,7 @@ import com.google.common.collect.Sets; import com.samskivert.depot.expression.ColumnExp; import com.samskivert.depot.expression.SQLExpression; import com.samskivert.depot.impl.DepotUtil; -import com.samskivert.util.StringUtil; -import com.samskivert.util.Tuple; +import com.samskivert.depot.util.Tuple2; /** * This class handles the construction of a Where clause from a set of multi-column keys. @@ -85,7 +81,7 @@ class MultiKeySet extends KeySet } @Override public String toString () { - return DepotUtil.justClassName(_pClass) + StringUtil.toString(_keys); + return DepotUtil.justClassName(_pClass) + Arrays.toString(_keys); } // note: this method will destructively modify its arguments @@ -102,11 +98,11 @@ class MultiKeySet extends KeySet Comparable maxValue = null; for (int column : columnsLeft) { - Tuple, Integer> colChunk = findBiggestChunk(keys, column); - if (colChunk.right > maxSize) { + Tuple2, Integer> colChunk = findBiggestChunk(keys, column); + if (colChunk.b > maxSize) { maxColumn = column; - maxSize = colChunk.right; - maxValue = colChunk.left; + maxSize = colChunk.b; + maxValue = colChunk.a; } } @@ -125,7 +121,7 @@ class MultiKeySet extends KeySet } // iterate key rows and find the most common value across those rows, in the given column - protected Tuple, Integer> findBiggestChunk (List[]> rows, int col) + protected Tuple2, Integer> findBiggestChunk (List[]> rows, int col) { int maxCount = 0; Comparable maxValue = null; @@ -146,7 +142,7 @@ class MultiKeySet extends KeySet maxValue = element; } } - return new Tuple, Integer>(maxValue, maxCount); + return new Tuple2, Integer>(maxValue, maxCount); } // find all the rows that contain the given chunk value in the given column. delete these diff --git a/src/main/java/com/samskivert/depot/PersistenceContext.java b/src/main/java/com/samskivert/depot/PersistenceContext.java index a18547b..cb2ea7a 100644 --- a/src/main/java/com/samskivert/depot/PersistenceContext.java +++ b/src/main/java/com/samskivert/depot/PersistenceContext.java @@ -17,14 +17,6 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; -import com.samskivert.io.PersistenceException; -import com.samskivert.util.StringUtil; - -import com.samskivert.jdbc.ConnectionProvider; -import com.samskivert.jdbc.DatabaseLiaison; -import com.samskivert.jdbc.JDBCUtil; -import com.samskivert.jdbc.LiaisonRegistry; - import com.samskivert.depot.CacheAdapter.CacheCategory; import com.samskivert.depot.CacheAdapter.CachedValue; import com.samskivert.depot.annotation.TableGenerator; @@ -37,6 +29,9 @@ import com.samskivert.depot.impl.KeyCacheKey; import com.samskivert.depot.impl.Modifier; import com.samskivert.depot.impl.Operation; import com.samskivert.depot.impl.SQLBuilder; +import com.samskivert.depot.impl.jdbc.DatabaseLiaison; +import com.samskivert.depot.impl.jdbc.JDBCUtil; +import com.samskivert.depot.impl.jdbc.LiaisonRegistry; import static com.samskivert.depot.Log.log; @@ -165,6 +160,15 @@ public class PersistenceContext return _cache; } + /** + * Returns an object that handles metadata management for Depot. This is not usually needed for + * normal operation. + */ + public DepotMetaData getMetaData () + { + return _meta; + } + /** * Returns whether schema migrations are allowed in this context. */ @@ -612,13 +616,7 @@ public class PersistenceContext } long preConnect = System.nanoTime(); - Connection conn; - try { - conn = connop.get(); - } catch (PersistenceException pe) { - throw new DatabaseException("Failed get connection [ident=" + _ident + - ", isRO=" + isReadOnly + "]", pe); - } + Connection conn = connop.get(); // wrap the connection in a proxy that will collect all opened statements List stmts = Lists.newArrayListWithCapacity(1); @@ -667,7 +665,7 @@ public class PersistenceContext // the MySQL JDBC driver has the annoying habit of including the embedded // exception stack trace in the message of their outer exception; if I want a // fucking stack trace, I'll call printStackTrace() thanksverymuch - String msg = StringUtil.split(String.valueOf(sqe), "\n")[0]; + String msg = String.valueOf(sqe).split("\n")[0]; log.info("Transient failure executing op, retrying [error=" + msg + "]."); } else { @@ -695,7 +693,7 @@ public class PersistenceContext } interface ConnOp { - Connection get () throws PersistenceException; + Connection get (); void done (Connection conn) throws SQLException; boolean fail (Connection conn, SQLException sqe); void release (Connection conn); @@ -706,7 +704,7 @@ public class PersistenceContext public NonTxOp (boolean readOnly) { this.readOnly = readOnly; } - public Connection get () throws PersistenceException { + public Connection get () { return _conprov.getConnection(_ident, readOnly); } public void done (Connection conn) throws SQLException { @@ -731,7 +729,7 @@ public class PersistenceContext public TxOp (Transaction tx) { this.tx = tx; } - public Connection get () throws PersistenceException { + public Connection get () { return tx.getConnection(); } public void done (Connection conn) throws SQLException { diff --git a/src/main/java/com/samskivert/depot/SchemaMigration.java b/src/main/java/com/samskivert/depot/SchemaMigration.java index 44d5a23..34174af 100644 --- a/src/main/java/com/samskivert/depot/SchemaMigration.java +++ b/src/main/java/com/samskivert/depot/SchemaMigration.java @@ -8,13 +8,12 @@ import java.sql.Connection; import java.sql.SQLException; import java.util.Map; -import com.samskivert.jdbc.ColumnDefinition; -import com.samskivert.jdbc.DatabaseLiaison; - import com.samskivert.depot.annotation.Column; import com.samskivert.depot.expression.ColumnExp; import com.samskivert.depot.impl.FieldMarshaller; import com.samskivert.depot.impl.Modifier; +import com.samskivert.depot.impl.jdbc.ColumnDefinition; +import com.samskivert.depot.impl.jdbc.DatabaseLiaison; import static com.google.common.base.Preconditions.checkArgument; import static com.samskivert.depot.Log.log; diff --git a/src/main/java/com/samskivert/depot/StaticConnectionProvider.java b/src/main/java/com/samskivert/depot/StaticConnectionProvider.java new file mode 100644 index 0000000..a193c13 --- /dev/null +++ b/src/main/java/com/samskivert/depot/StaticConnectionProvider.java @@ -0,0 +1,354 @@ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2012 Michael Bayne, et al. +// http://github.com/samskivert/samskivert/blob/master/COPYING + +package com.samskivert.depot; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.sql.*; +import java.util.*; + +import com.google.common.base.Strings; + +import static com.samskivert.depot.Log.log; + +/** + * The static connection provider generates JDBC connections based on configuration information + * provided via a properties file. It does no connection pooling and always returns the same + * connection for a particular identifier (unless that connection need be closed because of a + * connection failure, in which case it opens a new one the next time the connection is requested). + * + *

The configuration properties file should contain the following information: + * + *

+ * IDENT.driver=[jdbc driver class]
+ * IDENT.url=[jdbc driver url]
+ * IDENT.username=[jdbc username]
+ * IDENT.password=[jdbc password]
+ *
+ * [...]
+ * 
+ * + * Where IDENT is the database identifier for a particular database connection. When a + * particular database identifier is requested, the configuration information will be fetched from + * the properties. + * + *

Additionally, a default set of properties can be provided using the identifier + * default. Values not provided for a specific identifier will be sought from the + * defaults. For example: + * + *

+ * default.driver=[jdbc driver class]
+ * default.url=[jdbc driver class]
+ *
+ * IDENT1.username=[jdbc username]
+ * IDENT1.password=[jdbc password]
+ *
+ * IDENT2.username=[jdbc username]
+ * IDENT2.password=[jdbc password]
+ *
+ * [...]
+ * 
+ */ +public class StaticConnectionProvider implements ConnectionProvider +{ + /** Creates a provider for testing, using HSQLDB. */ + public static ConnectionProvider forTest (String dbname) { + Properties props = new Properties(); + props.setProperty("default.driver", "org.hsqldb.jdbcDriver"); + props.setProperty("default.username", "sa"); + props.setProperty("default.password", "none"); + props.setProperty("default.url", "jdbc:hsqldb:mem:" + dbname); + return new StaticConnectionProvider(props); + } + + /** + * Constructs a static connection provider which will load its configuration from a properties + * file accessible via the classpath of the running application and identified by the specified + * path. + * + * @param propPath the path (relative to the classpath) to the properties file that will be + * used for configuration information. + * + * @exception IOException thrown if an error occurs locating or loading the specified + * properties file. + */ + public StaticConnectionProvider (String propPath) throws IOException + { + this(loadProperties(propPath, ConnectionProvider.class.getClassLoader())); + } + + /** + * Constructs a static connection provider which will fetch its configuration information from + * the specified properties object. + * + * @param props the configuration for this connection provider. + */ + public StaticConnectionProvider (Properties props) + { + _props = props; + } + + // from ConnectionProvider + public String getURL (String ident) + { + Properties props = getSubProperties(_props, ident, DEFAULTS_KEY); + return props.getProperty("url"); + } + + // from ConnectionProvider + public Connection getConnection (String ident, boolean readOnly) + { + return getMapping(ident, readOnly).getConnection(ident); + } + + // from ConnectionProvider + public void releaseConnection (String ident, boolean readOnly, Connection conn) + { + // nothing to do here, all is well + } + + // from ConnectionProvider + public void connectionFailed ( + String ident, boolean readOnly, Connection conn, SQLException error) + { + String mapkey = ident + ":" + readOnly; + Mapping conmap = _idents.get(mapkey); + if (conmap == null) { + log.warning("Unknown connection failed!?", "key", mapkey); + } else { + conmap.closeConnection(ident); + } + } + + // from ConnectionProvider + public Connection getTxConnection (String ident) + { + return getMapping(ident, false).openConnection(ident, false); + } + + // from ConnectionProvider + public void releaseTxConnection (String ident, Connection conn) + { + close(conn, ident); + } + + // from ConnectionProvider + public void txConnectionFailed (String ident, Connection conn, SQLException error) + { + close(conn, ident); + } + + // from ConnectionProvider + public void shutdown () + { + // close all of the connections + for (Map.Entry entry : _keys.entrySet()) { + entry.getValue().closeConnection(entry.getKey()); + } + + // clear out our mapping tables + _keys.clear(); + _idents.clear(); + } + + protected Mapping getMapping (String ident, boolean readOnly) { + String mapkey = ident + ":" + readOnly; + Mapping conmap = _idents.get(mapkey); + if (conmap != null) return conmap; + + Properties props = getSubProperties(_props, ident, DEFAULTS_KEY); + Info info = new Info(ident, props); + + // if this is a read-only connection, we cache connections by username+url+readOnly to + // avoid making more that one connection to a particular database server + String key = info.username + "@" + info.url + ":" + readOnly; + conmap = _keys.get(key); + if (conmap == null) { + log.debug("Creating " + key + " for " + ident + "."); + _keys.put(key, conmap = new Mapping(key, info, readOnly)); + + } else { + log.debug("Reusing " + key + " for " + ident + "."); + } + + // cache the connection + _idents.put(mapkey, conmap); + + return conmap; + } + + protected static void close (Connection conn, String ident) { + try { + conn.close(); + } catch (SQLException sqe) { + log.warning("Error closing failed connection", "ident", ident, "error", sqe); + } + } + + protected static Properties loadProperties (String propPath, ClassLoader loader) + throws IOException { + InputStream in = loader.getResourceAsStream(propPath); + if (in == null) throw new FileNotFoundException(propPath); + Properties props = new Properties(); + props.load(in); + in.close(); + return props; + } + + protected static Properties getSubProperties (Properties source, String pref, String defPref) + { + Properties defs = new Properties(); + extractProperties(source, defs, defPref); + Properties dest = new Properties(defs); + extractProperties(source, dest, pref); + return dest; + } + + protected static void extractProperties (Properties source, Properties dest, String prefix) + { + // extend the prefix to contain a dot + prefix = prefix + "."; + int preflen = prefix.length(); + + // scan the source properties + Enumeration names = source.propertyNames(); + while (names.hasMoreElements()) { + String name = (String)names.nextElement(); + // skip unrelated properties + if (!name.startsWith(prefix)) { + continue; + } + // insert the value into the new properties minus the prefix + dest.put(name.substring(preflen), source.getProperty(name)); + } + } + + protected static class Info { + public final String ident, driver, url, username, password; + public final Boolean autoCommit; + + public Info (String ident, Properties props) { + this.ident = ident; + this.driver = requireProp(props, "driver", "No driver class specified"); + this.url = requireProp(props, "url", "No driver URL specified"); + this.username = requireProp(props, "username", "No driver username specified"); + this.password = props.getProperty("password", ""); + String ac = props.getProperty("autocommit"); + this.autoCommit = (ac == null) ? null : Boolean.valueOf(ac); + } + + protected String requireProp (Properties props, String name, String errmsg) { + String value = Strings.nullToEmpty(props.getProperty(name)).trim(); + if (value.isEmpty()) { + errmsg = "Unable to get connection. " + errmsg + " [ident=" + ident + "]"; + throw new DatabaseException(errmsg); + } + return value; + } + } + + /** Contains information on a particular connection to which any number of database identifiers + * can be mapped. */ + protected static class Mapping + { + /** The combination of username and JDBC url that uniquely identifies our database + * connection. */ + public final String key; + + public Mapping (String key, Info info, boolean readOnly) { + this.key = key; + _info = info; + _readOnly = readOnly; + } + + /** Returns the main connection for this mapping, (re)opening it if necessary. */ + public Connection getConnection (String ident) { + if (_conn == null) _conn = openConnection(ident, _info.autoCommit); + return _conn; + } + + /** Opens and returns a new connection to this mapping's database. */ + public Connection openConnection (String ident, Boolean autoCommit) { + // create an instance of the driver + Driver jdriver; + try { + jdriver = (Driver)Class.forName(_info.driver).newInstance(); + } catch (Exception e) { + throw new DatabaseException( + "Error loading driver [class=" + _info.driver + "].", e); + } + + // create the connection + Connection conn; + try { + Properties props = new Properties(); + props.put("user", _info.username); + props.put("password", _info.password); + conn = jdriver.connect(_info.url, props); + + } catch (SQLException sqe) { + throw new DatabaseException( + "Error creating database connection [driver=" + _info.driver + + ", url=" + _info.url + ", username=" + _info.username + "].", sqe); + } + + // if we were requested to configure auto-commit, then do so + if (autoCommit != null) { + try { + conn.setAutoCommit(autoCommit); + } catch (SQLException sqe) { + close(conn, ident); + throw new DatabaseException( + "Failed to configure auto-commit [key=" + key + ", ident=" + ident + + ", autoCommit=" + autoCommit + "].", sqe); + } + } + + // make the connection read-only to let the JDBC driver know that it can and should + // use the read-only mirror(s) + if (_readOnly) { + try { + conn.setReadOnly(true); + } catch (SQLException sqe) { + close(conn, ident); + throw new DatabaseException( + "Failed to make connection read-only [key=" + key + ", ident=" + ident + + "].", sqe); + } + } + return conn; + } + + /** + * Closes the main connection for this mapping, causing it to be reopened on the next call + * to {@link #getConnection}. + * @param ident the ident on behalf of which we are operating. + */ + public void closeConnection (String ident) { + if (_conn != null) { + close(_conn, ident); + _conn = null; + } + } + + protected final Info _info; + protected final boolean _readOnly; + protected Connection _conn; + } + + /** Our configuration in the form of a properties object. */ + protected Properties _props; + + /** A mapping from database identifier to connection records. */ + protected HashMap _idents = new HashMap(); + + /** A mapping from connection key to connection records. */ + protected HashMap _keys = new HashMap(); + + /** The key used as defaults for the database definitions. */ + protected static final String DEFAULTS_KEY = "default"; +} diff --git a/src/main/java/com/samskivert/depot/Stats.java b/src/main/java/com/samskivert/depot/Stats.java index 0e6c48b..252b414 100644 --- a/src/main/java/com/samskivert/depot/Stats.java +++ b/src/main/java/com/samskivert/depot/Stats.java @@ -4,7 +4,7 @@ package com.samskivert.depot; -import com.samskivert.util.Histogram; +import java.util.Arrays; import static com.samskivert.depot.Log.log; @@ -13,6 +13,77 @@ import static com.samskivert.depot.Log.log; */ public class Stats { + /** Used for tracking a set of values that fall into a discrete range of values. */ + public static class Histogram implements Cloneable + { + public Histogram (int minValue, int bucketWidth, int bucketCount) { + _minValue = minValue; + _maxValue = minValue + bucketWidth*bucketCount; + _bucketWidth = bucketWidth; + _buckets = new int[bucketCount]; + } + + /** + * Returns the array containing the bucket values. The zeroth element contains the count of + * all values less than minValue, the subsequent bucketCount + * elements contain the count of values falling into those buckets and the last element + * contains values greater than or equal to maxValue. + */ + public int[] getBuckets () { + return _buckets; + } + + /** + * Generates a terse summary of the count and contents of the values in this histogram. + */ + public String summarize () { + StringBuilder buf = new StringBuilder(); + buf.append(_count).append(":"); + for (int ii = 0; ii < _buckets.length; ii++) { + if (ii > 0) { + buf.append(","); + } + buf.append(_buckets[ii]); + } + return buf.toString(); + } + + @Override public Histogram clone () { + try { + Histogram chisto = (Histogram)super.clone(); + chisto._buckets = _buckets.clone(); + return chisto; + } catch (CloneNotSupportedException cnse) { + throw new AssertionError(cnse); + } + } + + @Override public String toString () { + return "[min=" + _minValue + ", max=" + _maxValue + ", bwidth=" + _bucketWidth + + ", buckets=" + Arrays.toString(_buckets) + "]"; + } + + void addValue (int value) { + if (value < _minValue) { + _buckets[0]++; + } else if (value >= _maxValue) { + _buckets[_buckets.length-1]++; + } else { + _buckets[(value-_minValue)/_bucketWidth]++; + } + _count++; + } + int size () { + return _count; + } + void clear () { + Arrays.fill(_buckets, 0); + } + + protected int _minValue, _maxValue, _bucketWidth, _count; + protected int[] _buckets; + } + /** * An immutable class used to report statistics on repository activity. Statistics are tracked * from the start of the VM and are never reset. diff --git a/src/main/java/com/samskivert/depot/Transaction.java b/src/main/java/com/samskivert/depot/Transaction.java index ac0af23..b1a5207 100644 --- a/src/main/java/com/samskivert/depot/Transaction.java +++ b/src/main/java/com/samskivert/depot/Transaction.java @@ -4,8 +4,6 @@ package com.samskivert.depot; -import com.samskivert.io.PersistenceException; -import com.samskivert.jdbc.ConnectionProvider; import java.sql.Connection; import java.sql.SQLException; @@ -122,7 +120,7 @@ public class Transaction { } } - Connection getConnection () throws PersistenceException + Connection getConnection () { if (_conn == null) _conn = ctx._conprov.getTxConnection(ctx._ident); return _conn; diff --git a/src/main/java/com/samskivert/depot/Transformers.java b/src/main/java/com/samskivert/depot/Transformers.java index 2c1924f..2ca5502 100644 --- a/src/main/java/com/samskivert/depot/Transformers.java +++ b/src/main/java/com/samskivert/depot/Transformers.java @@ -25,11 +25,9 @@ import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.primitives.Ints; -import com.samskivert.util.ByteEnum; -import com.samskivert.util.ByteEnumUtil; - import com.samskivert.depot.annotation.Column; import com.samskivert.depot.annotation.Transform; +import com.samskivert.depot.util.ByteEnum; import static com.samskivert.depot.Log.log; @@ -61,13 +59,13 @@ public class Transformers @Override public Integer toPersistent (Set value) { - return (value == null) ? null : ByteEnumUtil.setToInt(value); + return (value == null) ? null : ByteEnum.Util.setToInt(value); } @Override public Set fromPersistent (Integer encoded) { - return (encoded == null) ? null : ByteEnumUtil.intToSet(_eclass, encoded); + return (encoded == null) ? null : ByteEnum.Util.intToSet(_eclass, encoded); } /** The enum class token. */ diff --git a/src/main/java/com/samskivert/depot/clause/OrderBy.java b/src/main/java/com/samskivert/depot/clause/OrderBy.java index 8bc1ccb..59f863e 100644 --- a/src/main/java/com/samskivert/depot/clause/OrderBy.java +++ b/src/main/java/com/samskivert/depot/clause/OrderBy.java @@ -6,8 +6,9 @@ package com.samskivert.depot.clause; import java.util.Collection; +import com.google.common.collect.ObjectArrays; + import com.samskivert.depot.PersistentRecord; -import com.samskivert.util.ArrayUtil; import com.samskivert.depot.expression.SQLExpression; import com.samskivert.depot.impl.FragmentVisitor; import com.samskivert.depot.impl.expression.LiteralExp; @@ -89,8 +90,8 @@ public class OrderBy implements QueryClause */ public OrderBy thenAscending (SQLExpression value) { - return new OrderBy(ArrayUtil.append(_values, value), - ArrayUtil.append(_orders, Order.ASC)); + return new OrderBy(ObjectArrays.concat(_values, value), + ObjectArrays.concat(_orders, Order.ASC)); } /** @@ -98,8 +99,8 @@ public class OrderBy implements QueryClause */ public OrderBy thenDescending (SQLExpression value) { - return new OrderBy(ArrayUtil.append(_values, value), - ArrayUtil.append(_orders, Order.DESC)); + return new OrderBy(ObjectArrays.concat(_values, value), + ObjectArrays.concat(_orders, Order.DESC)); } // from SQLExpression diff --git a/src/main/java/com/samskivert/depot/impl/BuildVisitor.java b/src/main/java/com/samskivert/depot/impl/BuildVisitor.java index fb18309..f540d88 100644 --- a/src/main/java/com/samskivert/depot/impl/BuildVisitor.java +++ b/src/main/java/com/samskivert/depot/impl/BuildVisitor.java @@ -16,10 +16,6 @@ import com.google.common.collect.Maps; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import com.samskivert.depot.clause.Distinct; -import com.samskivert.util.ByteEnum; -import com.samskivert.util.Tuple; - import com.samskivert.depot.Exps; import com.samskivert.depot.Key; import com.samskivert.depot.PersistentRecord; @@ -39,29 +35,30 @@ import com.samskivert.depot.clause.WhereClause; import com.samskivert.depot.expression.*; import com.samskivert.depot.operator.Case; import com.samskivert.depot.operator.FullText; +import com.samskivert.depot.util.ByteEnum; +import com.samskivert.depot.util.Tuple2; import static com.samskivert.depot.Log.log; +import com.samskivert.depot.clause.Distinct; import com.samskivert.depot.impl.clause.CreateIndexClause; import com.samskivert.depot.impl.clause.DeleteClause; import com.samskivert.depot.impl.clause.DropIndexClause; import com.samskivert.depot.impl.clause.UpdateClause; -import com.samskivert.depot.impl.expression.AggregateFun; -import com.samskivert.depot.impl.expression.IntervalExp; -import com.samskivert.depot.impl.expression.LiteralExp; -import com.samskivert.depot.impl.expression.RandomExp; -import com.samskivert.depot.impl.expression.ValueExp; import com.samskivert.depot.impl.expression.AggregateFun.Average; import com.samskivert.depot.impl.expression.AggregateFun.Count; import com.samskivert.depot.impl.expression.AggregateFun.Every; import com.samskivert.depot.impl.expression.AggregateFun.Max; import com.samskivert.depot.impl.expression.AggregateFun.Min; import com.samskivert.depot.impl.expression.AggregateFun.Sum; +import com.samskivert.depot.impl.expression.AggregateFun; import com.samskivert.depot.impl.expression.ConditionalFun.Coalesce; import com.samskivert.depot.impl.expression.ConditionalFun.Greatest; import com.samskivert.depot.impl.expression.ConditionalFun.Least; import com.samskivert.depot.impl.expression.DateFun.DatePart; import com.samskivert.depot.impl.expression.DateFun.DateTruncate; import com.samskivert.depot.impl.expression.DateFun.Now; +import com.samskivert.depot.impl.expression.IntervalExp; +import com.samskivert.depot.impl.expression.LiteralExp; import com.samskivert.depot.impl.expression.NumericalFun.Abs; import com.samskivert.depot.impl.expression.NumericalFun.Ceil; import com.samskivert.depot.impl.expression.NumericalFun.Exp; @@ -75,12 +72,14 @@ import com.samskivert.depot.impl.expression.NumericalFun.Round; import com.samskivert.depot.impl.expression.NumericalFun.Sign; import com.samskivert.depot.impl.expression.NumericalFun.Sqrt; import com.samskivert.depot.impl.expression.NumericalFun.Trunc; +import com.samskivert.depot.impl.expression.RandomExp; import com.samskivert.depot.impl.expression.StringFun.Length; import com.samskivert.depot.impl.expression.StringFun.Lower; import com.samskivert.depot.impl.expression.StringFun.Position; import com.samskivert.depot.impl.expression.StringFun.Substring; import com.samskivert.depot.impl.expression.StringFun.Trim; import com.samskivert.depot.impl.expression.StringFun.Upper; +import com.samskivert.depot.impl.expression.ValueExp; import com.samskivert.depot.impl.operator.BinaryOperator; import com.samskivert.depot.impl.operator.Exists; import com.samskivert.depot.impl.operator.In; @@ -214,11 +213,11 @@ public abstract class BuildVisitor implements FragmentVisitor public Void visit (Case caseExp) { _builder.append("(case "); - for (Tuple, SQLExpression> tuple : caseExp.getWhenExps()) { + for (Tuple2, SQLExpression> tuple : caseExp.getWhenExps()) { _builder.append(" when "); - tuple.left.accept(this); + tuple.a.accept(this); _builder.append(" then "); - tuple.right.accept(this); + tuple.b.accept(this); } SQLExpression elseExp = caseExp.getElseExp(); if (elseExp != null) { @@ -511,8 +510,8 @@ public abstract class BuildVisitor implements FragmentVisitor public Void visit (CreateIndexClause createIndexClause) { if (!_allowComplexIndices) { - for (Tuple, Order> field : createIndexClause.getFields()) { - if (!(field.left instanceof ColumnExp)) { + for (Tuple2, Order> field : createIndexClause.getFields()) { + if (!(field.a instanceof ColumnExp)) { log.warning("This database can't handle complex indexes. Not creating.", "ixName", createIndexClause.getName()); return null; @@ -532,7 +531,7 @@ public abstract class BuildVisitor implements FragmentVisitor // turn off table abbreviations here _defaultType = createIndexClause.getPersistentClass(); boolean comma = false; - for (Tuple, Order> field : createIndexClause.getFields()) { + for (Tuple2, Order> field : createIndexClause.getFields()) { if (comma) { _builder.append(", "); } @@ -544,11 +543,11 @@ public abstract class BuildVisitor implements FragmentVisitor if (_allowComplexIndices) { _builder.append("("); } - field.left.accept(this); + field.a.accept(this); if (_allowComplexIndices) { _builder.append(")"); } - if (field.right == Order.DESC) { + if (field.b == Order.DESC) { // ascending is default, print nothing unless explicitly descending _builder.append(" desc"); } diff --git a/src/main/java/com/samskivert/depot/impl/DepotMarshaller.java b/src/main/java/com/samskivert/depot/impl/DepotMarshaller.java index 83e545d..d1a5915 100644 --- a/src/main/java/com/samskivert/depot/impl/DepotMarshaller.java +++ b/src/main/java/com/samskivert/depot/impl/DepotMarshaller.java @@ -18,6 +18,7 @@ import java.util.Map; import java.util.Set; import com.google.common.base.Function; +import com.google.common.base.MoreObjects; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; @@ -25,11 +26,6 @@ import com.google.common.collect.Maps; import com.google.common.collect.Sets; import static com.google.common.base.Preconditions.checkArgument; -import com.samskivert.jdbc.ColumnDefinition; -import com.samskivert.jdbc.DatabaseLiaison; -import com.samskivert.util.StringUtil; -import com.samskivert.util.Tuple; - import com.samskivert.depot.DatabaseException; import com.samskivert.depot.Key; import com.samskivert.depot.PersistenceContext; @@ -50,6 +46,9 @@ import com.samskivert.depot.clause.QueryClause; import com.samskivert.depot.expression.ColumnExp; import com.samskivert.depot.expression.SQLExpression; import com.samskivert.depot.impl.clause.CreateIndexClause; +import com.samskivert.depot.impl.jdbc.ColumnDefinition; +import com.samskivert.depot.impl.jdbc.DatabaseLiaison; +import com.samskivert.depot.util.Tuple2; import static com.samskivert.depot.Log.log; /** @@ -100,9 +99,9 @@ public class DepotMarshaller implements QueryMarshal // introspect on the class and create marshallers and indices for persistent fields List> fields = Lists.newArrayList(); - ListMultimap, Order>> namedFieldIndices = + ListMultimap, Order>> namedFieldIndices = ArrayListMultimap.create(); - ListMultimap, Order>> uniqueNamedFieldIndices = + ListMultimap, Order>> uniqueNamedFieldIndices = ArrayListMultimap.create(); for (Field field : _pClass.getFields()) { int mods = field.getModifiers(); @@ -183,8 +182,8 @@ public class DepotMarshaller implements QueryMarshal "Unique columns are implicitly indexed and should not be @Index'd."); String name = index.name().equals("") ? field.getName() + "Index" : index.name(); - Tuple, Order> entry = - new Tuple, Order>(fieldColumn, Order.ASC); + Tuple2, Order> entry = + new Tuple2, Order>(fieldColumn, Order.ASC); if (index.unique()) { checkArgument(!namedFieldIndices.containsKey(index.name()), "All @Index for a particular name must be unique or non-unique"); @@ -931,27 +930,27 @@ public class DepotMarshaller implements QueryMarshal protected CreateIndexClause buildIndex (String name, boolean unique, Object config) { - List, Order>> definition = Lists.newArrayList(); + List, Order>> definition = Lists.newArrayList(); if (config instanceof ColumnExp) { - definition.add(new Tuple, Order>((ColumnExp)config, Order.ASC)); + definition.add(new Tuple2, Order>((ColumnExp)config, Order.ASC)); } else if (config instanceof ColumnExp[]) { for (ColumnExp column : (ColumnExp[])config) { - definition.add(new Tuple, Order>(column, Order.ASC)); + definition.add(new Tuple2, Order>(column, Order.ASC)); } } else if (config instanceof SQLExpression) { - definition.add(new Tuple, Order>((SQLExpression)config, Order.ASC)); - } else if (config instanceof Tuple) { - @SuppressWarnings("unchecked") Tuple, Order> tuple = - (Tuple, Order>)config; + definition.add(new Tuple2, Order>((SQLExpression)config, Order.ASC)); + } else if (config instanceof Tuple2) { + @SuppressWarnings("unchecked") Tuple2, Order> tuple = + (Tuple2, Order>)config; definition.add(tuple); } else if (config instanceof List) { - @SuppressWarnings("unchecked") List, Order>> defs = - (List, Order>>)config; + @SuppressWarnings("unchecked") List, Order>> defs = + (List, Order>>)config; definition.addAll(defs); } else { throw new IllegalArgumentException( "Method '" + name + "' must return ColumnExp[], SQLExpression or " + - "List>"); + "List>"); } return new CreateIndexClause(_pClass, getTableName() + "_" + name, unique, definition); } @@ -1087,10 +1086,15 @@ public class DepotMarshaller implements QueryMarshal return true; } - @Override - public String toString () + @Override public String toString () { - return StringUtil.fieldsToString(this); + return MoreObjects.toStringHelper(this). + add("tableExists", tableExists). + add("tableColumns", tableColumns). + add("indexColumns", indexColumns). + add("pkName", pkName). + add("pkColumns", pkColumns). + toString(); } } diff --git a/src/main/java/com/samskivert/depot/impl/DepotMetaData.java b/src/main/java/com/samskivert/depot/impl/DepotMetaData.java index 9383f4c..a3dce06 100644 --- a/src/main/java/com/samskivert/depot/impl/DepotMetaData.java +++ b/src/main/java/com/samskivert/depot/impl/DepotMetaData.java @@ -13,13 +13,12 @@ import java.util.Map; import com.google.common.collect.Maps; -import com.samskivert.jdbc.ColumnDefinition; -import com.samskivert.jdbc.DatabaseLiaison; -import com.samskivert.jdbc.HsqldbLiaison; -import com.samskivert.jdbc.MySQLLiaison; -import com.samskivert.jdbc.PostgreSQLLiaison; - import com.samskivert.depot.PersistenceContext; +import com.samskivert.depot.impl.jdbc.ColumnDefinition; +import com.samskivert.depot.impl.jdbc.DatabaseLiaison; +import com.samskivert.depot.impl.jdbc.HsqldbLiaison; +import com.samskivert.depot.impl.jdbc.MySQLLiaison; +import com.samskivert.depot.impl.jdbc.PostgreSQLLiaison; /** * Does something extraordinary. @@ -140,6 +139,19 @@ public class DepotMetaData }) > 0; } + /** + * Removes the version for the specified persistent class. This is useful if the class's table + * is being dropped. + */ + public void clearVersion (final String pClass) { + _ctx.invoke(new Modifier.Simple() { + @Override protected String createQuery (DatabaseLiaison liaison) { + return "delete from " + liaison.tableSQL(SCHEMA_VERSION_TABLE) + + " where " + liaison.columnSQL(P_COLUMN) + " = '" + pClass + "'"; + } + }); + } + /** * Creates and return a new {@link SQLBuilder} for the appropriate dialect. * diff --git a/src/main/java/com/samskivert/depot/impl/ExpressionEvaluator.java b/src/main/java/com/samskivert/depot/impl/ExpressionEvaluator.java index 42c11e8..5395ee7 100644 --- a/src/main/java/com/samskivert/depot/impl/ExpressionEvaluator.java +++ b/src/main/java/com/samskivert/depot/impl/ExpressionEvaluator.java @@ -5,9 +5,11 @@ package com.samskivert.depot.impl; import java.lang.reflect.Field; +import java.util.Arrays; import com.samskivert.depot.Key; import com.samskivert.depot.PersistentRecord; +import com.samskivert.depot.util.Tuple2; import com.samskivert.depot.clause.Distinct; import com.samskivert.depot.clause.FieldDefinition; @@ -72,8 +74,6 @@ import com.samskivert.depot.impl.operator.In; import com.samskivert.depot.impl.operator.IsNull; import com.samskivert.depot.impl.operator.MultiOperator; import com.samskivert.depot.impl.operator.Not; -import com.samskivert.util.ArrayUtil; -import com.samskivert.util.Tuple; import static com.samskivert.depot.Log.log; @@ -130,7 +130,7 @@ public class ExpressionEvaluator { Object operand = in.getExpression().accept(this); return (operand instanceof NoValue) ? operand : - -1 != ArrayUtil.indexOf(in.getValues(), operand); + Arrays.asList(in.getValues()).contains(operand); } public Object visit (FullText.Match match) @@ -145,13 +145,13 @@ public class ExpressionEvaluator public Object visit (Case caseExp) { - for (Tuple, SQLExpression> exp : caseExp.getWhenExps()) { - Object result = exp.left.accept(this); + for (Tuple2, SQLExpression> exp : caseExp.getWhenExps()) { + Object result = exp.a.accept(this); if (result instanceof NoValue || !(result instanceof Boolean)) { - return new NoValue("Failed to evaluate case: " + exp.left + " -> " + result); + return new NoValue("Failed to evaluate case: " + exp.a + " -> " + result); } if (((Boolean) result).booleanValue()) { - return exp.right.accept(this); + return exp.b.accept(this); } } SQLExpression elseExp = caseExp.getElseExp(); diff --git a/src/main/java/com/samskivert/depot/impl/FieldMarshaller.java b/src/main/java/com/samskivert/depot/impl/FieldMarshaller.java index 965c803..03b090f 100644 --- a/src/main/java/com/samskivert/depot/impl/FieldMarshaller.java +++ b/src/main/java/com/samskivert/depot/impl/FieldMarshaller.java @@ -22,18 +22,15 @@ import com.google.common.collect.ImmutableMap; import static com.google.common.base.Preconditions.checkArgument; import com.samskivert.depot.DatabaseException; +import com.samskivert.depot.Log; import com.samskivert.depot.PersistentRecord; import com.samskivert.depot.Transformer; import com.samskivert.depot.annotation.Column; import com.samskivert.depot.annotation.Computed; import com.samskivert.depot.annotation.GeneratedValue; import com.samskivert.depot.annotation.Transform; -import com.samskivert.jdbc.ColumnDefinition; - -import com.samskivert.util.ByteEnum; -import com.samskivert.util.ByteEnumUtil; -import com.samskivert.util.Logger; -import com.samskivert.util.StringUtil; +import com.samskivert.depot.impl.jdbc.ColumnDefinition; +import com.samskivert.depot.util.ByteEnum; /** * Handles the marshalling and unmarshalling of a particular field of a persistent object. @@ -88,12 +85,12 @@ public abstract class FieldMarshaller return createTransformingMarshaller(xformer, field, xform); } catch (InstantiationException e) { throw new IllegalArgumentException( - Logger.format("Unable to create Transformer", "xclass", xform.value(), - "field", field), e); + Log.format("Unable to create Transformer", "xclass", xform.value(), + "field", field), e); } catch (IllegalAccessException e) { throw new IllegalArgumentException( - Logger.format("Unable to create Transformer", "xclass", xform.value(), - "field", field), e); + Log.format("Unable to create Transformer", "xclass", xform.value(), + "field", field), e); } } @@ -277,10 +274,8 @@ public abstract class FieldMarshaller } } } - if (column != null) { - if (!StringUtil.isBlank(column.name())) { - _columnName = column.name(); - } + if (column != null && !column.name().isEmpty()) { + _columnName = column.name(); } if (_computed != null) { @@ -673,11 +668,11 @@ public abstract class FieldMarshaller } @Override public ByteEnum getFromSet (ResultSet rs) throws SQLException { Number value = (Number)rs.getObject(getColumnName()); - return (value == null) ? null : ByteEnumUtil.fromByte(_eclass, value.byteValue()); + return (value == null) ? null : ByteEnum.Util.fromByte(_eclass, value.byteValue()); } @Override public ByteEnum getFromSet (ResultSet rs, int index) throws SQLException { Number value = (Number)rs.getObject(index); - return (value == null) ? null : ByteEnumUtil.fromByte(_eclass, value.byteValue()); + return (value == null) ? null : ByteEnum.Util.fromByte(_eclass, value.byteValue()); } @Override public void writeToObject (Object po, ByteEnum value) throws IllegalArgumentException, IllegalAccessException { diff --git a/src/main/java/com/samskivert/depot/impl/FindAllKeysQuery.java b/src/main/java/com/samskivert/depot/impl/FindAllKeysQuery.java index 2187065..438f706 100644 --- a/src/main/java/com/samskivert/depot/impl/FindAllKeysQuery.java +++ b/src/main/java/com/samskivert/depot/impl/FindAllKeysQuery.java @@ -13,7 +13,6 @@ import java.sql.SQLException; import com.google.common.collect.Lists; -import com.samskivert.jdbc.DatabaseLiaison; import com.samskivert.depot.DatabaseException; import com.samskivert.depot.Key; import com.samskivert.depot.PersistenceContext; @@ -21,6 +20,7 @@ import com.samskivert.depot.PersistentRecord; import com.samskivert.depot.Stats; import com.samskivert.depot.clause.QueryClause; import com.samskivert.depot.clause.SelectClause; +import com.samskivert.depot.impl.jdbc.DatabaseLiaison; import static com.samskivert.depot.Log.log; diff --git a/src/main/java/com/samskivert/depot/impl/FindAllQuery.java b/src/main/java/com/samskivert/depot/impl/FindAllQuery.java index 7088138..3fdeafb 100644 --- a/src/main/java/com/samskivert/depot/impl/FindAllQuery.java +++ b/src/main/java/com/samskivert/depot/impl/FindAllQuery.java @@ -33,8 +33,8 @@ import com.samskivert.depot.clause.QueryClause; import com.samskivert.depot.clause.SelectClause; import com.samskivert.depot.expression.ColumnExp; import com.samskivert.depot.expression.SQLExpression; +import com.samskivert.depot.impl.jdbc.DatabaseLiaison; import com.samskivert.depot.impl.operator.In; -import com.samskivert.jdbc.DatabaseLiaison; import static com.google.common.base.Preconditions.checkArgument; import static com.samskivert.depot.Log.log; diff --git a/src/main/java/com/samskivert/depot/impl/FindOneQuery.java b/src/main/java/com/samskivert/depot/impl/FindOneQuery.java index 9290940..1c4fe86 100644 --- a/src/main/java/com/samskivert/depot/impl/FindOneQuery.java +++ b/src/main/java/com/samskivert/depot/impl/FindOneQuery.java @@ -8,17 +8,17 @@ import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; -import com.samskivert.jdbc.DatabaseLiaison; +import com.samskivert.depot.CacheAdapter.CacheCategory; import com.samskivert.depot.CacheKey; import com.samskivert.depot.DatabaseException; import com.samskivert.depot.DepotRepository; import com.samskivert.depot.PersistenceContext; import com.samskivert.depot.PersistentRecord; import com.samskivert.depot.Stats; -import com.samskivert.depot.CacheAdapter.CacheCategory; import com.samskivert.depot.clause.QueryClause; import com.samskivert.depot.clause.SelectClause; import com.samskivert.depot.clause.WhereClause; +import com.samskivert.depot.impl.jdbc.DatabaseLiaison; import static com.samskivert.depot.Log.log; diff --git a/src/main/java/com/samskivert/depot/impl/HSQLBuilder.java b/src/main/java/com/samskivert/depot/impl/HSQLBuilder.java index 7f41273..f6708a2 100644 --- a/src/main/java/com/samskivert/depot/impl/HSQLBuilder.java +++ b/src/main/java/com/samskivert/depot/impl/HSQLBuilder.java @@ -7,6 +7,7 @@ package com.samskivert.depot.impl; import java.lang.reflect.Field; import java.sql.Connection; import java.sql.SQLException; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; @@ -23,21 +24,19 @@ import com.samskivert.depot.clause.OrderBy; import com.samskivert.depot.expression.ColumnExp; import com.samskivert.depot.expression.SQLExpression; import com.samskivert.depot.impl.expression.AggregateFun; -import com.samskivert.depot.impl.expression.DateFun.DatePart; import com.samskivert.depot.impl.expression.DateFun.DatePart.Part; +import com.samskivert.depot.impl.expression.DateFun.DatePart; import com.samskivert.depot.impl.expression.DateFun.DateTruncate; import com.samskivert.depot.impl.expression.LiteralExp; import com.samskivert.depot.impl.expression.NumericalFun; import com.samskivert.depot.impl.expression.StringFun.Lower; import com.samskivert.depot.impl.expression.ValueExp; +import com.samskivert.depot.impl.jdbc.ColumnDefinition; import com.samskivert.depot.impl.operator.BitAnd; import com.samskivert.depot.impl.operator.BitOr; import com.samskivert.depot.impl.operator.Like; import com.samskivert.depot.impl.operator.MultiOperator; import com.samskivert.depot.operator.FullText; -import com.samskivert.util.ArrayUtil; - -import com.samskivert.jdbc.ColumnDefinition; public class HSQLBuilder extends SQLBuilder @@ -92,10 +91,11 @@ public class HSQLBuilder getFullTextIndex(match.getDefinition().getName()).fields(); // explode the query into words - String[] ftsWords = match.getDefinition().getQuery().toLowerCase().split("\\W+"); - if (ftsWords.length > 0 && ftsWords[0].length() == 0) { + List ftsWords = Arrays.asList( + match.getDefinition().getQuery().toLowerCase().split("\\W+")); + if (ftsWords.size() > 0 && ftsWords.get(0).length() == 0) { // if the query led with whitespace, the first 'word' will be empty; strip it - ftsWords = ArrayUtil.splice(ftsWords, 0, 1); + ftsWords = ftsWords.subList(1, ftsWords.size()); } // now iterate over the cartesian product of the query words & the fields diff --git a/src/main/java/com/samskivert/depot/impl/IdentityValueGenerator.java b/src/main/java/com/samskivert/depot/impl/IdentityValueGenerator.java index c912094..2cd564f 100644 --- a/src/main/java/com/samskivert/depot/impl/IdentityValueGenerator.java +++ b/src/main/java/com/samskivert/depot/impl/IdentityValueGenerator.java @@ -8,8 +8,8 @@ import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; -import com.samskivert.jdbc.DatabaseLiaison; import com.samskivert.depot.annotation.GeneratedValue; +import com.samskivert.depot.impl.jdbc.DatabaseLiaison; /** * Generates primary keys using an identity column. diff --git a/src/main/java/com/samskivert/depot/impl/Modifier.java b/src/main/java/com/samskivert/depot/impl/Modifier.java index a1bdec8..fb78697 100644 --- a/src/main/java/com/samskivert/depot/impl/Modifier.java +++ b/src/main/java/com/samskivert/depot/impl/Modifier.java @@ -15,7 +15,7 @@ import com.samskivert.depot.Key; import com.samskivert.depot.PersistenceContext; import com.samskivert.depot.PersistentRecord; import com.samskivert.depot.Stats; -import com.samskivert.jdbc.DatabaseLiaison; +import com.samskivert.depot.impl.jdbc.DatabaseLiaison; /** * Encapsulates a modification of persistent objects. diff --git a/src/main/java/com/samskivert/depot/impl/MySQLBuilder.java b/src/main/java/com/samskivert/depot/impl/MySQLBuilder.java index 9a0f1e6..07d35c0 100644 --- a/src/main/java/com/samskivert/depot/impl/MySQLBuilder.java +++ b/src/main/java/com/samskivert/depot/impl/MySQLBuilder.java @@ -6,15 +6,14 @@ package com.samskivert.depot.impl; import java.sql.Connection; import java.sql.SQLException; - import java.util.Map; import java.util.Set; -import com.samskivert.depot.clause.Distinct; -import com.samskivert.util.StringUtil; +import com.google.common.base.Joiner; import com.samskivert.depot.PersistentRecord; import com.samskivert.depot.annotation.FullTextIndex; +import com.samskivert.depot.clause.Distinct; import com.samskivert.depot.clause.OrderBy; import com.samskivert.depot.expression.ColumnExp; import com.samskivert.depot.operator.FullText; @@ -188,10 +187,10 @@ public class MySQLBuilder String[] searchTerms = fullText.getQuery().toLowerCase().trim().split("\\W+"); if (fullText.isMatchAll()) { // Prepend every search term with a plus for ANDing - return '+' + StringUtil.join(searchTerms, " +"); + return "+" + Joiner.on(" +").join(searchTerms); } else { // Use the default OR matching - return StringUtil.join(searchTerms, " "); + return Joiner.on(" ").join(searchTerms); } } diff --git a/src/main/java/com/samskivert/depot/impl/Operation.java b/src/main/java/com/samskivert/depot/impl/Operation.java index aec0c43..0853c51 100644 --- a/src/main/java/com/samskivert/depot/impl/Operation.java +++ b/src/main/java/com/samskivert/depot/impl/Operation.java @@ -10,7 +10,7 @@ import java.sql.Statement; import com.samskivert.depot.PersistenceContext; import com.samskivert.depot.Stats; -import com.samskivert.jdbc.DatabaseLiaison; +import com.samskivert.depot.impl.jdbc.DatabaseLiaison; /** * An abstraction that encompasses both {@link Fetcher} and {@link Modifier} operations. diff --git a/src/main/java/com/samskivert/depot/impl/PostgreSQL4Builder.java b/src/main/java/com/samskivert/depot/impl/PostgreSQL4Builder.java index e2501e0..fb1613c 100644 --- a/src/main/java/com/samskivert/depot/impl/PostgreSQL4Builder.java +++ b/src/main/java/com/samskivert/depot/impl/PostgreSQL4Builder.java @@ -14,7 +14,7 @@ import java.sql.Timestamp; import com.samskivert.depot.DatabaseException; import com.samskivert.depot.Exps; import com.samskivert.depot.impl.operator.In; -import com.samskivert.util.ByteEnum; +import com.samskivert.depot.util.ByteEnum; /** * Specializes our PostgreSQL builder for JDBC4. diff --git a/src/main/java/com/samskivert/depot/impl/PostgreSQLBuilder.java b/src/main/java/com/samskivert/depot/impl/PostgreSQLBuilder.java index 5c11132..fae7e39 100644 --- a/src/main/java/com/samskivert/depot/impl/PostgreSQLBuilder.java +++ b/src/main/java/com/samskivert/depot/impl/PostgreSQLBuilder.java @@ -5,17 +5,13 @@ package com.samskivert.depot.impl; import java.lang.reflect.Field; - import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; - import java.util.Map; import java.util.Set; -import com.samskivert.jdbc.DatabaseLiaison; -import com.samskivert.jdbc.LiaisonRegistry; -import com.samskivert.util.StringUtil; +import com.google.common.base.Joiner; import com.samskivert.depot.Exps; import com.samskivert.depot.PersistentRecord; @@ -30,6 +26,8 @@ import com.samskivert.depot.impl.expression.DateFun.DatePart; import com.samskivert.depot.impl.expression.DateFun.DateTruncate; import com.samskivert.depot.impl.expression.IntervalExp; import com.samskivert.depot.impl.expression.RandomExp; +import com.samskivert.depot.impl.jdbc.DatabaseLiaison; +import com.samskivert.depot.impl.jdbc.LiaisonRegistry; public class PostgreSQLBuilder extends SQLBuilder @@ -284,7 +282,7 @@ public class PostgreSQLBuilder // String[] searchTerms = fullText.getQuery().toLowerCase().trim().split("\\W+"); String operator = fullText.isMatchAll() ? "&" : "|"; - return StringUtil.join(searchTerms, operator); + return Joiner.on(operator).join(searchTerms); } // Translate the mildly abstracted full-text parser/dictionary configuration support diff --git a/src/main/java/com/samskivert/depot/impl/SQLBuilder.java b/src/main/java/com/samskivert/depot/impl/SQLBuilder.java index 799633e..095d9f0 100644 --- a/src/main/java/com/samskivert/depot/impl/SQLBuilder.java +++ b/src/main/java/com/samskivert/depot/impl/SQLBuilder.java @@ -19,8 +19,8 @@ import com.samskivert.depot.annotation.Column; import com.samskivert.depot.annotation.FullTextIndex; import com.samskivert.depot.annotation.GeneratedValue; import com.samskivert.depot.clause.QueryClause; -import com.samskivert.jdbc.ColumnDefinition; -import com.samskivert.util.ByteEnum; +import com.samskivert.depot.impl.jdbc.ColumnDefinition; +import com.samskivert.depot.util.ByteEnum; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; diff --git a/src/main/java/com/samskivert/depot/impl/TableValueGenerator.java b/src/main/java/com/samskivert/depot/impl/TableValueGenerator.java index 778f9cf..9c988ff 100644 --- a/src/main/java/com/samskivert/depot/impl/TableValueGenerator.java +++ b/src/main/java/com/samskivert/depot/impl/TableValueGenerator.java @@ -13,9 +13,8 @@ import java.util.Arrays; import com.samskivert.depot.annotation.GeneratedValue; import com.samskivert.depot.annotation.TableGenerator; - -import com.samskivert.jdbc.ColumnDefinition; -import com.samskivert.jdbc.DatabaseLiaison; +import com.samskivert.depot.impl.jdbc.ColumnDefinition; +import com.samskivert.depot.impl.jdbc.DatabaseLiaison; /** * Generates primary keys using an external table . diff --git a/src/main/java/com/samskivert/depot/impl/ValueGenerator.java b/src/main/java/com/samskivert/depot/impl/ValueGenerator.java index 309fe35..493fadf 100644 --- a/src/main/java/com/samskivert/depot/impl/ValueGenerator.java +++ b/src/main/java/com/samskivert/depot/impl/ValueGenerator.java @@ -9,8 +9,8 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; -import com.samskivert.jdbc.DatabaseLiaison; import com.samskivert.depot.annotation.GeneratedValue; +import com.samskivert.depot.impl.jdbc.DatabaseLiaison; import static com.samskivert.depot.Log.log; diff --git a/src/main/java/com/samskivert/depot/impl/clause/CreateIndexClause.java b/src/main/java/com/samskivert/depot/impl/clause/CreateIndexClause.java index 8236cd2..eddcaf1 100644 --- a/src/main/java/com/samskivert/depot/impl/clause/CreateIndexClause.java +++ b/src/main/java/com/samskivert/depot/impl/clause/CreateIndexClause.java @@ -7,12 +7,11 @@ package com.samskivert.depot.impl.clause; import java.util.Collection; import java.util.List; -import com.samskivert.util.Tuple; - import com.samskivert.depot.PersistentRecord; import com.samskivert.depot.clause.OrderBy.Order; import com.samskivert.depot.clause.QueryClause; import com.samskivert.depot.expression.SQLExpression; +import com.samskivert.depot.util.Tuple2; import com.samskivert.depot.impl.FragmentVisitor; @@ -27,7 +26,7 @@ public class CreateIndexClause * database. */ public CreateIndexClause (Class pClass, String name, boolean unique, - List, Order>> fields) + List, Order>> fields) { _pClass = pClass; _name = name; @@ -50,7 +49,7 @@ public class CreateIndexClause return _unique; } - public List,Order>> getFields () + public List,Order>> getFields () { return _fields; } @@ -72,5 +71,5 @@ public class CreateIndexClause protected boolean _unique; /** The components of the index, e.g. columns or functions of columns. */ - protected List,Order>> _fields; + protected List,Order>> _fields; } diff --git a/src/main/java/com/samskivert/depot/impl/expression/Function.java b/src/main/java/com/samskivert/depot/impl/expression/Function.java index 3e731cd..4e95e98 100644 --- a/src/main/java/com/samskivert/depot/impl/expression/Function.java +++ b/src/main/java/com/samskivert/depot/impl/expression/Function.java @@ -6,10 +6,11 @@ package com.samskivert.depot.impl.expression; import java.util.Collection; +import com.google.common.base.Joiner; + import com.samskivert.depot.PersistentRecord; import com.samskivert.depot.expression.FluentExp; import com.samskivert.depot.expression.SQLExpression; -import com.samskivert.util.StringUtil; public interface Function { @@ -91,7 +92,7 @@ public interface Function @Override public String toString () { - return getCanonicalFunctionName() + "(" + StringUtil.join(_args, ", ") + ")"; + return getCanonicalFunctionName() + "(" + Joiner.on(", ").join(_args) + ")"; } } } diff --git a/src/main/java/com/samskivert/depot/impl/jdbc/BaseLiaison.java b/src/main/java/com/samskivert/depot/impl/jdbc/BaseLiaison.java new file mode 100644 index 0000000..b4ac6f3 --- /dev/null +++ b/src/main/java/com/samskivert/depot/impl/jdbc/BaseLiaison.java @@ -0,0 +1,361 @@ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2012 Michael Bayne, et al. +// http://github.com/samskivert/samskivert/blob/master/COPYING + +package com.samskivert.depot.impl.jdbc; + +import com.google.common.base.Joiner; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +import static com.samskivert.depot.Log.log; + +/** + * A superclass to help with the shrinking subset of SQL our supported dialects can agree on, + * or when there is disagreement, implement the most standard-compliant version and let the + * dialectal sub-classes override. + */ +public abstract class BaseLiaison implements DatabaseLiaison +{ + public BaseLiaison () { + super(); + } + + // we override all the interface methods here so that our subclasses can use @Override without + // incurring the wrath of the Eclipse Java 1.5 compiler; Jesus Fuck, why do we support 1.5? + + // from DatabaseLiaison + public abstract boolean matchesURL (String url); + + // from DatabaseLiaison + public abstract boolean isDuplicateRowException (SQLException sqe); + + // from DatabaseLiaison + public abstract boolean isTransientException (SQLException sqe); + + @Deprecated + public int lastInsertedId (Connection conn, String table, String column) throws SQLException { + return lastInsertedId(conn, null, table, column); + } + + // from DatabaseLiaison + public int lastInsertedId (Connection conn, Statement istmt, String table, String column) + throws SQLException + { + // if this JDBC driver supports getGeneratedKeys, use it! + if (istmt != null && conn.getMetaData().supportsGetGeneratedKeys()) { + ResultSet rs = istmt.getGeneratedKeys(); + if (rs.next()) { + return rs.getInt(column); + } + } + return fetchLastInsertedId(conn, table, column); + } + + /** + * Requests the last inserted id for the specified table and column. This is used if a JDBC + * driver does not support {@code getGeneratedKeys} or an attempt to use that failed. + */ + protected int fetchLastInsertedId (Connection conn, String table, String column) + throws SQLException + { + throw new SQLException( + "Unable to obtain last inserted id [table=" + table + ", column=" + column + "]"); + } + + // from DatabaseLiaison + public boolean tableExists (Connection conn, String name) throws SQLException + { + ResultSet rs = conn.getMetaData().getTables(null, null, name, null); + while (rs.next()) { + String tname = rs.getString("TABLE_NAME"); + if (name.equals(tname)) { + return true; + } + } + return false; + } + + // from DatabaseLiaison + public boolean tableContainsColumn (Connection conn, String table, String column) + throws SQLException + { + ResultSet rs = conn.getMetaData().getColumns(null, null, table, column); + while (rs.next()) { + String tname = rs.getString("TABLE_NAME"); + String cname = rs.getString("COLUMN_NAME"); + if (tname.equals(table) && cname.equals(column)) { + return true; + } + } + return false; + } + + // from DatabaseLiaison + public boolean tableContainsIndex (Connection conn, String table, String index) + throws SQLException + { + ResultSet rs = conn.getMetaData().getIndexInfo(null, null, table, false, true); + while (rs.next()) { + String tname = rs.getString("TABLE_NAME"); + String iname = rs.getString("INDEX_NAME"); + if (tname.equals(table) && index.equals(iname)) { + return true; + } + } + return false; + } + + // from DatabaseLiaison + public boolean addIndexToTable (Connection conn, String table, List columns, + String ixName, boolean unique) throws SQLException + { + if (tableContainsIndex(conn, table, ixName)) { + return false; + } + ixName = (ixName != null ? ixName : + Joiner.on("_").join(columns.toArray(new String[columns.size()]))); + + StringBuilder update = new StringBuilder("CREATE "); + if (unique) { + update.append("UNIQUE "); + } + update.append("INDEX ").append(indexSQL(ixName)).append(" ON "). + append(tableSQL(table)).append(" ("); + appendColumns(columns, update); + update.append(")"); + + executeQuery(conn, update.toString()); + log("Database index '" + ixName + "' added to table '" + table + "'"); + return true; + } + + // from DatabaseLiaison + public void addPrimaryKey (Connection conn, String table, List columns) + throws SQLException + { + StringBuilder fields = new StringBuilder("("); + appendColumns(columns, fields); + fields.append(")"); + String update = "ALTER TABLE " + tableSQL(table) + " ADD PRIMARY KEY " + fields.toString(); + + executeQuery(conn, update); + log("Primary key " + fields + " added to table '" + table + "'"); + } + + // from DatabaseLiaison + public void dropIndex (Connection conn, String table, String index) throws SQLException + { + executeQuery(conn, "DROP INDEX " + columnSQL(index)); + } + + // from DatabaseLiaison + public void dropPrimaryKey (Connection conn, String table, String pkName) throws SQLException + { + executeQuery(conn, "ALTER TABLE " + tableSQL(table) + + " DROP CONSTRAINT " + columnSQL(pkName)); + } + + // from DatabaseLiaison + public boolean addColumn (Connection conn, String table, String column, String definition, + boolean check) throws SQLException + { + if (check && tableContainsColumn(conn, table, column)) { + return false; + } + + executeQuery(conn, "ALTER TABLE " + tableSQL(table) + " ADD COLUMN " + + columnSQL(column) + " " + definition); + log("Database column '" + column + "' added to table '" + table + "'."); + return true; + } + + // from DatabaseLiaison + public boolean addColumn (Connection conn, String table, String column, + ColumnDefinition newColumnDef, boolean check) + throws SQLException + { + if (check && tableContainsColumn(conn, table, column)) { + return false; + } + + executeQuery(conn, "ALTER TABLE " + tableSQL(table) + " ADD COLUMN " + + columnSQL(column) + " " + expandDefinition(newColumnDef)); + log("Database column '" + column + "' added to table '" + table + "'."); + return true; + } + + // from DatabaseLiaison + public boolean changeColumn (Connection conn, String table, String column, String type, + Boolean nullable, Boolean unique, String defaultValue) + throws SQLException + { + String defStr = expandDefinition(type, nullable != null ? nullable : false, + unique != null ? unique : false, defaultValue); + + executeQuery(conn, "ALTER TABLE " + tableSQL(table) + " CHANGE " + + columnSQL(column) + " " + columnSQL(column) + " " + defStr); + log("Database column '" + column + "' of table '" + table + "' modified to have " + + "definition '" + defStr + "'."); + return true; + } + + // from DatabaseLiaison + public boolean renameColumn (Connection conn, String table, String from, String to, + ColumnDefinition newColumnDef) throws SQLException + { + executeQuery(conn, "ALTER TABLE " + tableSQL(table) + " RENAME COLUMN " + + columnSQL(from) + " TO " + columnSQL(to)); + log("Renamed column '" + from + "' on table '" + table + "' to '" + to + "'"); + return true; + } + + // from DatabaseLiaison + public abstract void createGenerator (Connection conn, String tableName, String columnName, + int initialValue) + throws SQLException; + + // from DatabaseLiaison + public abstract void deleteGenerator (Connection conn, String tableName, String columnName) + throws SQLException; + + // from DatabaseLiaison + public boolean dropColumn (Connection conn, String table, String column) throws SQLException + { + if (!tableContainsColumn(conn, table, column)) { + return false; + } + executeQuery(conn, "ALTER TABLE " + tableSQL(table) + " DROP COLUMN " + columnSQL(column)); + log("Database column '" + column + "' removed from table '" + table + "'."); + return true; + } + + // from DatabaseLiaison + public boolean createTableIfMissing (Connection conn, String table, List columns, + List declarations, + List primaryKeyColumns) throws SQLException { + return createTableIfMissing(conn, table, columns, declarations, null, primaryKeyColumns); + } + + // from DatabaseLiaison + public boolean createTableIfMissing (Connection conn, String table, List columns, + List definitions, + List> uniqueConstraintColumns, + List primaryKeyColumns) + throws SQLException + { + if (tableExists(conn, table)) { + return false; + } + if (columns.size() != definitions.size()) { + throw new IllegalArgumentException("Column name and definition number mismatch"); + } + + StringBuilder builder = new StringBuilder("CREATE TABLE "). + append(tableSQL(table)).append(" ("); + for (int ii = 0; ii < columns.size(); ii ++) { + if (ii > 0) { + builder.append(", "); + } + builder.append(columnSQL(columns.get(ii))).append(" "); + builder.append(expandDefinition(definitions.get(ii))); + } + + if (uniqueConstraintColumns != null) { + for (List uCols : uniqueConstraintColumns) { + builder.append(", UNIQUE ("); + appendColumns(uCols, builder); + builder.append(")"); + } + } + + if (primaryKeyColumns != null && !primaryKeyColumns.isEmpty()) { + builder.append(", PRIMARY KEY ("); + appendColumns(primaryKeyColumns, builder); + builder.append(")"); + } + + builder.append(")"); + + executeQuery(conn, builder.toString()); + log("Database table '" + table + "' created."); + return true; + } + + // from DatabaseLiaison + public boolean dropTable (Connection conn, String name) throws SQLException + { + if (!tableExists(conn, name)) { + return false; + } + executeQuery(conn, "DROP TABLE " + tableSQL(name)); + log("Table '" + name + "' dropped."); + return true; + } + + // from DatabaseLiaison + public abstract String tableSQL (String table); + + // from DatabaseLiaison + public abstract String columnSQL (String column); + + // from DatabaseLiaison + public abstract String indexSQL (String index); + + /** + * Create an SQL string that summarizes a column definition in that format generally accepted + * in table creation and column addition statements, e.g. {@code INTEGER UNIQUE NOT NULL + * DEFAULT 100}. + */ + public String expandDefinition (ColumnDefinition def) + { + return expandDefinition(def.type, def.nullable, def.unique, def.defaultValue); + } + + protected int executeQuery (Connection conn, String query) throws SQLException + { + Statement stmt = conn.createStatement(); + try { + return stmt.executeUpdate(query); + } finally { + JDBCUtil.close(stmt); + } + } + + protected String expandDefinition (String type, boolean nullable, boolean unique, + String defaultValue) + { + StringBuilder builder = new StringBuilder(type); + if (!nullable) { + builder.append(" NOT NULL"); + } + if (unique) { + builder.append(" UNIQUE"); + } + // append the default value if one was specified + if (defaultValue != null) { + builder.append(" DEFAULT ").append(defaultValue); + } + return builder.toString(); + } + + /** Escapes {@code columns} with {@link #columnSQL}, appends (comma-sepped) to {@code buf}. */ + protected void appendColumns (Iterable columns, StringBuilder buf) { + int ii = 0; + for (String column : columns) { + if (ii++ > 0) { + buf.append(", "); + } + buf.append(columnSQL(column)); + } + } + + protected void log (String message) { + log.info(message); + } +} diff --git a/src/main/java/com/samskivert/depot/impl/jdbc/ColumnDefinition.java b/src/main/java/com/samskivert/depot/impl/jdbc/ColumnDefinition.java new file mode 100644 index 0000000..fb55f5b --- /dev/null +++ b/src/main/java/com/samskivert/depot/impl/jdbc/ColumnDefinition.java @@ -0,0 +1,36 @@ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2012 Michael Bayne, et al. +// http://github.com/samskivert/samskivert/blob/master/COPYING + +package com.samskivert.depot.impl.jdbc; + +/** + * An object representing the configuration of a typical SQL column. + */ +public class ColumnDefinition +{ + public String type; + public boolean nullable; + public boolean unique; + public String defaultValue; + + public ColumnDefinition () + { + this(null); + } + + public ColumnDefinition (String type) + { + this(type, false, false, null); + } + + public ColumnDefinition ( + String type, boolean nullable, boolean unique, String defaultValue) + { + this.type = type; + this.nullable = nullable; + this.unique = unique; + this.defaultValue = defaultValue; + } +} diff --git a/src/main/java/com/samskivert/depot/impl/jdbc/DatabaseLiaison.java b/src/main/java/com/samskivert/depot/impl/jdbc/DatabaseLiaison.java new file mode 100644 index 0000000..84f58b6 --- /dev/null +++ b/src/main/java/com/samskivert/depot/impl/jdbc/DatabaseLiaison.java @@ -0,0 +1,222 @@ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2012 Michael Bayne, et al. +// http://github.com/samskivert/samskivert/blob/master/COPYING + +package com.samskivert.depot.impl.jdbc; + +import java.sql.Statement; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +/** + * Despite good intentions, JDBC and SQL do not provide a unified interface to all databases. There + * remain idiosyncrasies that must be worked around when making code interact with different + * database servers. The database liaison encapsulates the code needed to straighten out the curves + * and curve out the straights. + */ +public interface DatabaseLiaison +{ + /** + * Indicates whether or not this database liaison is the proper liaison for the specified + * database URL. + * + * @return true if we should use this liaison for connections created with the supplied URL, + * false if we should not. + */ + public boolean matchesURL (String url); + + /** + * Determines whether or not the supplied SQL exception was caused by a duplicate row being + * inserted into a table with a unique key. + * + * @return true if the exception was caused by the insertion of a duplicate row, false if not. + */ + public boolean isDuplicateRowException (SQLException sqe); + + /** + * Determines whether or not the supplied SQL exception is a transient failure, meaning one + * that is not related to the SQL being executed, but instead to the environment at the time of + * execution, like the connection to the database having been lost. + * + * @return true if the exception was thrown due to a transient failure, false if not. + */ + public boolean isTransientException (SQLException sqe); + + /** @deprecated Use version that takes the insert statement. */ + @Deprecated + public int lastInsertedId (Connection conn, String table, String column) throws SQLException; + + /** + * Attempts as dialect-agnostic an interface as possible to the ability of certain databases to + * auto-generated numerical values for i.e. key columns; there is MySQL's AUTO_INCREMENT and + * PostgreSQL's DEFAULT nextval(sequence), for example. + * + * @param istmt the insert statement that generated the keys. May be null if the ORM doesn't + * have the statement handy. + * @return the requested inserted id. + * @throws SQLException if we are unable to obtain the last inserted id. + */ + public int lastInsertedId (Connection conn, Statement istmt, String table, String column) + throws SQLException; + + /** + * Initializes the column value auto-generator described in {@link #lastInsertedId}. This + * should be idempotent (meaning the generator may already exist in which case this method + * should have no negative effect like resetting it). + */ + public void createGenerator (Connection conn, String tableName, String columnName, + int initialValue) + throws SQLException; + + /** + * Deletes the column value auto-generator described in {@link #lastInsertedId}. + */ + public void deleteGenerator (Connection conn, String tableName, String columnName) + throws SQLException; + + /** + * Drops the given column from the given table. Returns true or false if the database did or + * did not report a schema modification. + */ + public boolean dropColumn (Connection conn, String table, String column) + throws SQLException; + + /** + * Adds a named index to a table on the given columns. Returns true or false if the database + * did or did not report a schema modification. + */ + public boolean addIndexToTable (Connection conn, String table, List columns, + String index, boolean unique) + throws SQLException; + + /** + * Drops the named index from the given table. + */ + public void dropIndex (Connection conn, String table, String index) + throws SQLException; + + /** + * Adds a primary key to a table of the given name and on the given columns. Returns true or + * false if the database did nor did not report a schema modification. + */ + public void addPrimaryKey (Connection conn, String table, List columns) + throws SQLException; + + /** + * Deletes the primary key from a table, if it exists. + */ + public void dropPrimaryKey (Connection conn, String table, String pkName) + throws SQLException; + + /** + * Adds a column to a table with the given definition. Tests for the previous existence of + * the column iff 'check' is true. + */ + public boolean addColumn (Connection conn, String table, String column, String definition, + boolean check) + throws SQLException; + + /** + * Adds a column to a table with the given definition. Tests for the previous existence of + * the column iff 'check' is true. + */ + public boolean addColumn (Connection conn, String table, String column, + ColumnDefinition columnDef, boolean check) + throws SQLException; + + /** + * Alter the definition, but not the name, of a given column on a given table. Returns true or + * false if the database did or did not report a schema modification. Any of the nullable, + * unique and defaultValue arguments may be null, in which case the implementation will attempt + * to leave that aspect of the column unchanged. + */ + public boolean changeColumn (Connection conn, String table, String column, String type, + Boolean nullable, Boolean unique, String defaultValue) + throws SQLException; + + /** + * Alter the name, but not the definition, of a given column on a given table. Returns true or + * false if the database did or did not report a schema modification. + * + * @param columnDef the full definition of the new column, including its new name (MySQL + * requires this for a column rename). + */ + public boolean renameColumn (Connection conn, String table, String oldColumn, String newColumn, + ColumnDefinition columnDef) + throws SQLException; + + /** + * Created a new table of the given name with the given column names and column definitions; + * the given set of unique constraints (or null) and the given primary key columns (or null). + * Returns true if the table was successfully created, false if it already existed. + */ + public boolean createTableIfMissing (Connection conn, String table, List columns, + List declarations, + List primaryKeyColumns) + throws SQLException; + + /** + * Created a new table of the given name with the given column names and column definitions; + * the given set of unique constraints (or null) and the given primary key columns (or null). + * Returns true if the table was successfully created, false if it already existed. + */ + public boolean createTableIfMissing (Connection conn, String table, List columns, + List declarations, + List> uniqueConstraintColumns, + List primaryKeyColumns) + throws SQLException; + + /** + * Create an SQL string that summarizes a column definition in that format generally accepted + * in table creation and column addition statements, e.g. {@code INTEGER UNIQUE NOT NULL + * DEFAULT 100} + */ + public String expandDefinition (ColumnDefinition coldef); + + /** + * Returns true if the specified table exists and contains an index of the specified name; + * false if either conditions does not hold true. Note: the names are case sensitive. + */ + public boolean tableContainsIndex (Connection conn, String table, String index) + throws SQLException; + + /** + * Returns true if the specified table exists and contains a column with the specified name; + * false if either condition does not hold true. Note: the names are case sensitive. + */ + public boolean tableContainsColumn (Connection conn, String table, String column) + throws SQLException; + + /** + * Returns true if the table with the specified name exists, false if it does not. + * Note: the table name is case sensitive. + */ + public boolean tableExists (Connection conn, String name) + throws SQLException; + + /** + * Drops the given table and returns true if the table exists, else returns false. + * Note: the table name is case sensitive. + */ + public boolean dropTable (Connection conn, String name) + throws SQLException; + + /** + * Returns the proper SQL to identify a table. Some databases require table names to be quoted. + */ + public String tableSQL (String table); + + /** + * Returns the proper SQL to identify a column. Some databases require columns names to be + * quoted. + */ + public String columnSQL (String column); + + /** + * Returns the proper SQL to identify an index. Some databases require index names to be + * quoted. + */ + public String indexSQL (String index); +} diff --git a/src/main/java/com/samskivert/depot/impl/jdbc/DefaultLiaison.java b/src/main/java/com/samskivert/depot/impl/jdbc/DefaultLiaison.java new file mode 100644 index 0000000..fe7305f --- /dev/null +++ b/src/main/java/com/samskivert/depot/impl/jdbc/DefaultLiaison.java @@ -0,0 +1,66 @@ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2012 Michael Bayne, et al. +// http://github.com/samskivert/samskivert/blob/master/COPYING + +package com.samskivert.depot.impl.jdbc; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * The default liaison is used if no other liaison could be matched for a particular database + * connection. It isn't very smart or useful but we need something. + */ +public class DefaultLiaison extends BaseLiaison +{ + @Override // from DatabaseLiaison + public boolean matchesURL (String url) + { + return true; + } + + @Override // from DatabaseLiaison + public boolean isDuplicateRowException (SQLException sqe) + { + return false; + } + + @Override // from DatabaseLiaison + public boolean isTransientException (SQLException sqe) + { + return false; + } + + @Override // from DatabaseLiaison + public void createGenerator (Connection conn, String tableName, String columnName, int initValue) + throws SQLException + { + // nothing doing + } + + @Override // from DatabaseLiaison + public void deleteGenerator (Connection conn, String tableName, String columnName) + throws SQLException + { + // nothing doing + } + + @Override // from DatabaseLiaison + public String columnSQL (String column) + { + return column; + } + + @Override // from DatabaseLiaison + public String tableSQL (String table) + { + return table; + } + + @Override // from DatabaseLiaison + public String indexSQL (String index) + { + return index; + } +} diff --git a/src/main/java/com/samskivert/depot/impl/jdbc/HsqldbLiaison.java b/src/main/java/com/samskivert/depot/impl/jdbc/HsqldbLiaison.java new file mode 100644 index 0000000..a35f27c --- /dev/null +++ b/src/main/java/com/samskivert/depot/impl/jdbc/HsqldbLiaison.java @@ -0,0 +1,204 @@ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2012 Michael Bayne, et al. +// http://github.com/samskivert/samskivert/blob/master/COPYING + +package com.samskivert.depot.impl.jdbc; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.sql.SQLException; + +import static com.samskivert.depot.Log.log; + +/** + * Handles liaison for HSQLDB. + */ +public class HsqldbLiaison extends BaseLiaison +{ + @Override // from DatabaseLiaison + public boolean matchesURL (String url) + { + return url.startsWith("jdbc:hsqldb"); + } + + @Override // from DatabaseLiaison + public String columnSQL (String column) + { + return "\"" + column + "\""; + } + + @Override // from DatabaseLiaison + public String tableSQL (String table) + { + return "\"" + table + "\""; + } + + @Override // from DatabaseLiaison + public String indexSQL (String index) + { + return "\"" + index + "\""; + } + + @Override // from DatabaseLiaison + public void createGenerator (Connection conn, String tableName, + String columnName, int initValue) + throws SQLException + { + if (initValue == 1) { + return; // that's the default! yay, do nothing + } + + Statement stmt = conn.createStatement(); + try { + stmt.execute("alter table " + tableSQL(tableName) + + " alter column " + columnSQL(columnName) + + " restart with " + initValue); + } finally { + JDBCUtil.close(stmt); + } + log("Initial value of " + tableName + ":" + columnName + " set to " + initValue + "."); + } + + @Override // from DatabaseLiaison + public void deleteGenerator (Connection conn, String tableName, String columnName) + throws SQLException + { + // HSQL's IDENTITY() does not create any database entities that we need to delete + } + + @Override + protected int fetchLastInsertedId (Connection conn, String table, String column) + throws SQLException + { + // HSQL does not keep track of per-table-and-column insertion data, so we are pretty much + // going on blind faith here that we're fetching the right ID. In the overwhelming number + // of cases that will be so, but it's still not pretty. + Statement stmt = null; + try { + stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("call IDENTITY()"); + return rs.next() ? rs.getInt(1) : super.fetchLastInsertedId(conn, table, column); + } finally { + JDBCUtil.close(stmt); + } + } + + @Override // from DatabaseLiaison + public boolean isTransientException (SQLException sqe) + { + return false; // no known transient exceptions for HSQLDB + } + + @Override // from DatabaseLiaison + public boolean isDuplicateRowException (SQLException sqe) + { + // Violation of unique constraint SYS_PK_51: duplicate value(s) for column(s) FOO + // integrity constraint violation: unique constraint or index violation; SYS_CT_10057 + String msg = sqe.getMessage(); + return (msg != null && (msg.contains("duplicate value(s)") || + msg.contains("unique constraint or index violation"))); + } + + // BaseLiaison's implementation of table creation accepts unique constraints both as + // part of the column definition and as a separate argument, and merrily passes this + // duality onto the database. Postgres and MySQL both handle this fine but HSQL seems + // to simply not allow uniqueness in the column definitions. So, for HSQL, we transfer + // uniqueness from the ColumnDefinitions to the uniqueConstraintColumns before we pass + // it in to the super implementation. + // + // TODO: Consider making this the general MO instead of a subclass override. In fact + // it may be that uniqueness should be removed from ColumnDefinition. + @Override // from DatabaseLiaison + public boolean createTableIfMissing (Connection conn, String table, + List columns, List defns, + List> uniqueColumns, List pkColumns) + throws SQLException + { + if (columns.size() != defns.size()) { + throw new IllegalArgumentException("Column name and definition number mismatch"); + } + + // note the set of single column unique constraints already provided (see method comment) + Set seenUniques = new HashSet(); + if (uniqueColumns != null) { + for (List udef : uniqueColumns) { + if (udef.size() == 1) { + seenUniques.addAll(udef); + } + } + } + // primary key columns are also considered implicitly unique as of HSQL 2.2.4, and it will + // freak out if we also try to include them in the UNIQUE clause, so add those too + seenUniques.addAll(pkColumns); + + // lazily create a copy of uniqueColumns, if needed + List> newUniques = uniqueColumns; + + // go through the columns and find any that are unique; these we replace with a non-unique + // variant, and instead add a new entry to the table unique constraint + List newDefns = new ArrayList(defns.size()); + for (int ii = 0; ii < defns.size(); ii ++) { + ColumnDefinition def = defns.get(ii); + if (!def.unique) { + newDefns.add(def); + continue; + } + + // let's be nice and not mutate the caller's object + newDefns.add( + new ColumnDefinition(def.type, def.nullable, false, def.defaultValue)); + // if a uniqueness constraint for this column was not in the primaryKeys or + // uniqueColumns parameters, add the column to uniqueCsts + if (!seenUniques.contains(columns.get(ii))) { + if (newUniques == uniqueColumns) { + newUniques = new ArrayList>(uniqueColumns); + } + newUniques.add(Collections.singletonList(columns.get(ii))); + } + } + + // now call the real implementation with our modified data + return super.createTableIfMissing(conn, table, columns, newDefns, newUniques, pkColumns); + } + + @Override // from DatabaseLiaison + protected String expandDefinition (String type, boolean nullable, boolean unique, + String defaultValue) + { + StringBuilder builder = new StringBuilder(type); + + // append the default value if one was specified + if (defaultValue != null) { + if ("IDENTITY".equals(defaultValue)) { + // this is a blatant hack, we need this method to join Depot's SQLBuilder + builder.append(" GENERATED BY DEFAULT AS IDENTITY (START WITH 1)"); + } else { + builder.append(" DEFAULT ").append(defaultValue); + } + } + + if (!nullable) { + builder.append(" NOT NULL"); + } + if (unique) { + throw new IllegalArgumentException("HSQL can't deal with column uniqueness here"); + } + + return builder.toString(); + } + + @Override + protected void log (String message) { + // HSQL is generally used as a test database, so we don't generally want to hear a bunch of + // spam about table creation, etc. every time we start up/run tests + log.debug(message); + } +} diff --git a/src/main/java/com/samskivert/depot/impl/jdbc/JDBCUtil.java b/src/main/java/com/samskivert/depot/impl/jdbc/JDBCUtil.java new file mode 100644 index 0000000..9c1edb1 --- /dev/null +++ b/src/main/java/com/samskivert/depot/impl/jdbc/JDBCUtil.java @@ -0,0 +1,647 @@ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2012 Michael Bayne, et al. +// http://github.com/samskivert/samskivert/blob/master/COPYING + +package com.samskivert.depot.impl.jdbc; + +import java.io.UnsupportedEncodingException; +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.List; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import com.google.common.base.Joiner; + +import static com.samskivert.depot.Log.log; + +/** + * A repository for JDBC related utility functions. + */ +public class JDBCUtil +{ + /** Used for {@link #batchQuery}. */ + public interface BatchProcessor + { + /** + * Called for each row returned during our batch query. Do not advance the result set, + * simply query its values with the get methods. + */ + public void process (ResultSet row) + throws SQLException; + } + + /** + * Closes the supplied JDBC statement and gracefully handles being passed null (by doing + * nothing). + */ + public static void close (Statement stmt) + throws SQLException + { + if (stmt != null) { + stmt.close(); + } + } + + /** + * Closes the supplied JDBC connection and gracefully handles being passed null (by doing + * nothing). + */ + public static void close (Connection conn) + throws SQLException + { + if (conn != null) { + conn.close(); + } + } + + /** + * Wraps the given connection in a proxied instance that will add all statements returned by + * methods called on the proxy (such as {@link Connection#createStatement}) to the supplied + * list. Thus you can create the proxy, pass the proxy to code that creates and uses statements + * and then close any statements created by the code that operated on that Connection before + * returning it to a pool, for example. + */ + public static Connection makeCollector (final Connection conn, final List stmts) + { + return (Connection)Proxy.newProxyInstance( + Connection.class.getClassLoader(), PROXY_IFACES, new InvocationHandler() { + public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { + Object result = method.invoke(conn, args); + if (result instanceof Statement) { + stmts.add((Statement)result); + } + return result; + } + }); + } + + /** + * Calls stmt.executeUpdate() on the supplied statement, checking to see that it + * returns the expected update count and throwing an exception if it does not. + */ + public static void checkedUpdate (PreparedStatement stmt, int expectedCount) + throws SQLException + { + int modified = stmt.executeUpdate(); + if (modified != expectedCount) { + String err = "Statement did not modify expected number of rows [stmt=" + stmt + + ", expected=" + expectedCount + ", modified=" + modified + "]"; + throw new SQLException(err); + } + } + + /** + * Calls stmt.executeUpdate() on the supplied statement with the supplied query, + * checking to see that it returns the expected update count and throwing an exception if it + * does not. + */ + public static void checkedUpdate (Statement stmt, String query, int expectedCount) + throws SQLException + { + int modified = stmt.executeUpdate(query); + if (modified != expectedCount) { + String err = "Statement did not modify expected number of rows [stmt=" + stmt + + ", expected=" + expectedCount + ", modified=" + modified + "]"; + throw new SQLException(err); + } + } + + /** + * Calls stmt.executeUpdate() on the supplied statement, checking to see that it + * returns the expected update count and logging a warning if it does not. + */ + public static void warnedUpdate (PreparedStatement stmt, int expectedCount) + throws SQLException + { + int modified = stmt.executeUpdate(); + if (modified != expectedCount) { + log.warning("Statement did not modify expected number of rows", "stmt", stmt, + "expected", expectedCount, "modified", modified); + } + } + + /** + * Issues a query with a potentially large number of keys in batches. For example, you might + * have 10,000 ids that you wish to use in an "in" clause, but don't trust the database to be + * smart about optimizing that many keys, so instead you use batchQuery like so: + *
{@code
+     *    Collection keys = ...;
+     *    String query = "select NAME from USERS where USER_ID in (#KEYS)";
+     *    JDBCUtil.BatchProcessor proc = new JDBCUtil.BatchProcessor() {
+     *        public void process (ResultSet row) {
+     *            String name = rs.getString(1);
+     *            // do whatever with name
+     *        }
+     *    };
+     *    JDBCUtil.batchQuery(conn, query, keys, false, 500, proc);
+     * }
+ * + * @param query the SQL query to run for each batch with the string #KEYS# in the + * place where the batch of keys should be substituted. + * @param escapeKeys if true, {@link #escape} will be called on each key to escape any + * dangerous characters and wrap the key in quotes. + * @param batchSize the number of keys at a time to substitute in for #KEYS#. + */ + public static void batchQuery (Connection conn, String query, Collection keys, + boolean escapeKeys, int batchSize, BatchProcessor processor) + throws SQLException + { + Statement stmt = conn.createStatement(); + try { + Iterator itr = keys.iterator(); + while (itr.hasNext()) { + // group one batch of keys together + StringBuilder buf = new StringBuilder(); + for (int ii = 0; ii < batchSize && itr.hasNext(); ii++) { + if (ii > 0) { + buf.append(","); + } + String key = String.valueOf(itr.next()); + buf.append(escapeKeys ? escape(key) : key); + } + + // issue the query with that batch + String squery = query.replace("#KEYS#", buf.toString()); + ResultSet rs = stmt.executeQuery(squery); + while (rs.next()) { + processor.process(rs); + } + } + + } finally { + close(stmt); + } + } + + /** + * Calls stmt.executeUpdate() on the supplied statement with the supplied query, + * checking to see that it returns the expected update count and logging a warning if it does + * not. + */ + public static void warnedUpdate ( + Statement stmt, String query, int expectedCount) + throws SQLException + { + int modified = stmt.executeUpdate(query); + if (modified != expectedCount) { + log.warning("Statement did not modify expected number of rows", "stmt", stmt, + "expected", expectedCount, "modified", modified); + } + } + + /** + * Converts the date to a string and surrounds it in single-quotes via the escape method. If the + * date is null, returns null. + */ + public String quote (Date date) + { + return (date == null) ? null : escape(String.valueOf(date)); + } + + /** + * Escapes any single quotes in the supplied text and wraps it in single quotes to make it safe + * for embedding into a database query. + */ + public static String escape (String text) + { + text = text.replace("\\", "\\\\"); + return "'" + text.replace("'", "\\'") + "'"; + } + + /** + * Escapes a list of values, separating the escaped values by commas. See {@link + * #escape(String)}. + */ + public static String escape (Object[] values) + { + StringBuilder buf = new StringBuilder(); + for (int ii = 0; ii < values.length; ii++) { + if (ii > 0) { + buf.append(", "); + } + buf.append(escape(String.valueOf(values[ii]))); + } + return buf.toString(); + } + + /** + * Many databases simply fail to handle Unicode text properly and this routine provides a + * common workaround which is to represent a UTF-8 string as an ISO-8859-1 string. If you don't + * need to use the database's collation routines, this allows you to do pretty much exactly + * what you want at the expense of having to jigger and dejigger every goddamned string that + * might contain multibyte characters every time you access the database. Three cheers for + * progress! + */ + public static String jigger (String text) + { + if (text == null) { + return null; + } + try { + return new String(text.getBytes("UTF8"), "8859_1"); + } catch (UnsupportedEncodingException uee) { + log.warning("Jigger failed", uee); + return text; + } + } + + /** + * Reverses {@link #jigger}. + */ + public static String unjigger (String text) + { + if (text == null) { + return null; + } + try { + return new String(text.getBytes("8859_1"), "UTF8"); + } catch (UnsupportedEncodingException uee) { + log.warning("Unjigger failed", uee); + return text; + } + } + + /** + * Utility method to jigger the specified string so that it's safe to use in a regular + * Statement. + */ + public static String safeJigger (String text) + { + return jigger(text).replace("'", "\\'"); + } + + /** + * Used to programatically create a database table. Does nothing if the table already exists. + * + * @return true if the table was created, false if it already existed. + */ + public static boolean createTableIfMissing ( + Connection conn, String table, String[] definition, String postamble) + throws SQLException + { + if (tableExists(conn, table)) { + return false; + } + + Statement stmt = conn.createStatement(); + try { + stmt.executeUpdate("create table " + table + + "(" + Joiner.on(", ").join(definition) + ") " + postamble); + } finally { + close(stmt); + } + + log.info("Database table '" + table + "' created."); + return true; + } + + /** + * Returns true if the table with the specified name exists, false if it does + * not. Note: the table name is case sensitive. + */ + public static boolean tableExists (Connection conn, String name) + throws SQLException + { + boolean matched = false; + ResultSet rs = conn.getMetaData().getTables("", "", name, null); + while (rs.next()) { + String tname = rs.getString("TABLE_NAME"); + if (name.equals(tname)) { + matched = true; + } + } + return matched; + } + + /** + * Returns true if the table with the specified name exists and contains a column with the + * specified name, false if either condition does not hold true. Note: the names are + * case sensitive. + */ + public static boolean tableContainsColumn (Connection conn, String table, String column) + throws SQLException + { + boolean matched = false; + ResultSet rs = conn.getMetaData().getColumns("", "", table, column); + while (rs.next()) { + String tname = rs.getString("TABLE_NAME"); + String cname = rs.getString("COLUMN_NAME"); + if (tname.equals(table) && cname.equals(column)) { + matched = true; + } + } + return matched; + } + + /** + * Returns true if the index on the specified column exists for the specified table, false if + * it does not. Optionally you can specifiy a non null index name, and the table will be + * checked to see if it contains that specifically named index. Note: the names are + * case sensitive. + */ + public static boolean tableContainsIndex ( + Connection conn, String table, String column, String index) + throws SQLException + { + boolean matched = false; + ResultSet rs = conn.getMetaData().getIndexInfo("", "", table, false, true); + while (rs.next()) { + String tname = rs.getString("TABLE_NAME"); + String cname = rs.getString("COLUMN_NAME"); + String iname = rs.getString("INDEX_NAME"); + if (index == null) { + if (tname.equals(table) && cname.equals(column)) { + matched = true; + } + } else if (index.equals(iname)) { + matched = true; + } + } + return matched; + } + + /** + * Returns true if the specified table contains a primary key on the specified column. + */ + public static boolean tableContainsPrimaryKey (Connection conn, String table, String column) + throws SQLException + { + boolean matched = false; + ResultSet rs = conn.getMetaData().getPrimaryKeys("", "", table); + while (rs.next()) { + String tname = rs.getString("TABLE_NAME"); + String cname = rs.getString("COLUMN_NAME"); + if (tname.equals(table) && cname.equals(column)) { + matched = true; + } + } + return matched; + } + + /** + * Returns the name of the index for the specified column in the specified table. + */ + public static String getIndexName (Connection conn, String table, + String column) + throws SQLException + { + ResultSet rs = conn.getMetaData().getIndexInfo("", "", table, false, true); + while (rs.next()) { + String tname = rs.getString("TABLE_NAME"); + String cname = rs.getString("COLUMN_NAME"); + String iname = rs.getString("INDEX_NAME"); + if (tname.equals(table) && cname.equals(column)) { + return iname; + } + } + return null; + } + + /** + * Returns the type (as specified in {@link java.sql.Types} for the specified column in the + * specified table. + */ + public static int getColumnType (Connection conn, String table, + String column) + throws SQLException + { + ResultSet rs = getColumnMetaData(conn, table, column); + try { + return rs.getInt("DATA_TYPE"); + } finally { + rs.close(); + } + } + + /** + * Determines whether or not the specified column accepts null values. + * + * @return true if the column accepts null values, false if it does not (or its nullability is + * unknown) + */ + public static boolean isColumnNullable (Connection conn, String table, + String column) + throws SQLException + { + ResultSet rs = getColumnMetaData(conn, table, column); + try { + return rs.getString("IS_NULLABLE").equals("YES"); + } finally { + rs.close(); + } + } + + /** + * Returns the size for the specified column in the specified table. For char or date types + * this is the maximum number of characters, for numeric or decimal types this is the + * precision. + */ + public static int getColumnSize (Connection conn, String table, String column) + throws SQLException + { + ResultSet rs = getColumnMetaData(conn, table, column); + try { + return rs.getInt("COLUMN_SIZE"); + } finally { + rs.close(); + } + } + + /** + * Returns a string representation of the default value for the specified column in the + * specified table. This may be null. + */ + public static String getColumnDefaultValue (Connection conn, String table, String column) + throws SQLException + { + ResultSet rs = getColumnMetaData(conn, table, column); + try { + return rs.getString("COLUMN_DEF"); + } finally { + rs.close(); + } + } + + /** + * Adds a column (with name 'cname' and definition 'cdef') to the specified table. + * + * @param afterCname (optional) the name of the column after which to add the new column. + * + * @return true if the column was added, false if it already existed. + */ + public static boolean addColumn ( + Connection conn, String table, String cname, String cdef, String afterCname) + throws SQLException + { + if (tableContainsColumn(conn, table, cname)) { +// Log.info("Database table '" + table + "' already has column '" + cname + "'."); + return false; + } + + String update = "ALTER TABLE " + table + " ADD COLUMN " + cname + " " + cdef; + if (afterCname != null) { + update += " AFTER " + afterCname; + } + PreparedStatement stmt = null; + try { + stmt = conn.prepareStatement(update); + stmt.executeUpdate(); + } finally { + close(stmt); + } + log.info("Database column '" + cname + "' added to table '" + table + "'."); + return true; + } + + /** + * Changes a column's definition. Takes a full column definition 'cdef' (including the name of + * the column) with which to replace the specified column 'cname'. + * + * NOTE: A handy thing you can do with this is to rename a column by providing a column + * definition that has a different name, but the same column type. + */ + public static void changeColumn (Connection conn, String table, String cname, String cdef) + throws SQLException + { + String update = "ALTER TABLE " + table + " CHANGE " + cname + " " + cdef; + PreparedStatement stmt = null; + try { + stmt = conn.prepareStatement(update); + stmt.executeUpdate(); + } finally { + close(stmt); + } + log.info("Database column '" + cname + "' of table '" + table + + "' modified to have this def '" + cdef + "'."); + } + + /** + * Removes a column from the specified table. + * + * @return true if the column was dropped, false if it did not exist in the first place. + */ + public static boolean dropColumn (Connection conn, String table, String cname) + throws SQLException + { + if (!tableContainsColumn(conn, table, cname)) { + return false; + } + + String update = "ALTER TABLE " + table + " DROP COLUMN " + cname; + PreparedStatement stmt = null; + try { + stmt = conn.prepareStatement(update); + if (stmt.executeUpdate() == 1) { + log.info("Database index '" + cname + "' removed from table '" + table + "'."); + } + } finally { + close(stmt); + } + return true; + } + + /** + * Removes a named index from the specified table. + * + * @return true if the index was dropped, false if it did not exist in the first place. + */ + public static boolean dropIndex (Connection conn, String table, String cname, String iname) + throws SQLException + { + if (!tableContainsIndex(conn, table, cname, iname)) { + return false; + } + + String update = "ALTER TABLE " + table + " DROP INDEX " + iname; + PreparedStatement stmt = null; + try { + stmt = conn.prepareStatement(update); + if (stmt.executeUpdate() == 1) { + log.info("Database index '" + iname + "' removed from table '" + table + "'."); + } + } finally { + close(stmt); + } + return true; + } + + /** + * Removes the primary key from the specified table. + */ + public static void dropPrimaryKey (Connection conn, String table) + throws SQLException + { + String update = "ALTER TABLE " + table + " DROP PRIMARY KEY"; + PreparedStatement stmt = null; + try { + stmt = conn.prepareStatement(update); + if (stmt.executeUpdate() == 1) { + log.info("Database primary key removed from '" + table + "'."); + } + } finally { + close(stmt); + } + } + + /** + * Adds an index on the specified column (cname) to the specified table. Optionally supply an + * index name, otherwise the index is named after the column. + * + * @return true if the index was added, false if it already existed. + */ + public static boolean addIndexToTable ( + Connection conn, String table, String cname, String iname) + throws SQLException + { + if (tableContainsIndex(conn, table, cname, iname)) { +// Log.info("Database table '" + table + "' already has an index " + +// "on column '" + cname + "'" + +// (iname != null ? " named '" + iname + "'." : ".")); + return false; + } + + String idx_name = (iname != null ? iname : cname); + String update = "CREATE INDEX " + idx_name + " on " + table + "(" + cname + ")"; + PreparedStatement stmt = null; + try { + stmt = conn.prepareStatement(update); + stmt.executeUpdate(); + } finally { + close(stmt); + } + log.info("Database index '" + idx_name + "' added to table '" + table + "'"); + return true; + } + + /** + * Helper function for {@link #getColumnType}, etc. + */ + protected static ResultSet getColumnMetaData (Connection conn, String table, String column) + throws SQLException + { + ResultSet rs = conn.getMetaData().getColumns("", "", table, column); + while (rs.next()) { + String tname = rs.getString("TABLE_NAME"); + String cname = rs.getString("COLUMN_NAME"); + if (tname.equals(table) && cname.equals(column)) { + return rs; + } + } + throw new SQLException("Table or Column not defined. [table=" + table + + ", col=" + column + "]."); + } + + /** Used by {@link #makeCollector}. */ + protected static final Class[] PROXY_IFACES = { Connection.class }; +} diff --git a/src/main/java/com/samskivert/depot/impl/jdbc/LiaisonRegistry.java b/src/main/java/com/samskivert/depot/impl/jdbc/LiaisonRegistry.java new file mode 100644 index 0000000..bd13ecc --- /dev/null +++ b/src/main/java/com/samskivert/depot/impl/jdbc/LiaisonRegistry.java @@ -0,0 +1,81 @@ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2012 Michael Bayne, et al. +// http://github.com/samskivert/samskivert/blob/master/COPYING + +package com.samskivert.depot.impl.jdbc; + +import java.sql.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import static com.samskivert.depot.Log.log; + +/** + * The liaison registry provides access to the appropriate database liaison implementation for a + * particular database connection. + */ +public class LiaisonRegistry +{ + /** + * Fetch the appropriate database liaison for the supplied URL, which should be the same string + * that would be used to configure a connection to the database. + */ + public static DatabaseLiaison getLiaison (String url) + { + if (url == null) throw new NullPointerException("URL must not be null"); + // see if we already have a liaison mapped for this connection + DatabaseLiaison liaison = _mappings.get(url); + if (liaison == null) { + // scan the list looking for a matching liaison + for (DatabaseLiaison candidate : _liaisons) { + if (candidate.matchesURL(url)) { + liaison = candidate; + break; + } + } + + // if we didn't find a matching liaison, use the default + if (liaison == null) { + log.warning("Unable to match liaison for database. Using default.", "url", url); + liaison = new DefaultLiaison(); + } + + // map this URL to this liaison + _mappings.put(url, liaison); + } + + return liaison; + } + + /** + * Fetch the appropriate database liaison for the supplied database connection. + */ + public static DatabaseLiaison getLiaison (Connection conn) + throws SQLException + { + return getLiaison(conn.getMetaData().getURL()); + } + + protected static void registerLiaisonClass (Class lclass) + { + // create a new instance and stick it on our list + try { + _liaisons.add(lclass.newInstance()); + } catch (Exception e) { + log.warning("Unable to instantiate liaison", "class", lclass.getName(), "error", e); + } + } + + protected static ArrayList _liaisons = new ArrayList(); + protected static Map _mappings = new HashMap(); + + // register our liaison classes + static { + registerLiaisonClass(MySQLLiaison.class); + registerLiaisonClass(PostgreSQLLiaison.class); + registerLiaisonClass(HsqldbLiaison.class); + } +} diff --git a/src/main/java/com/samskivert/depot/impl/jdbc/MySQLLiaison.java b/src/main/java/com/samskivert/depot/impl/jdbc/MySQLLiaison.java new file mode 100644 index 0000000..1950c49 --- /dev/null +++ b/src/main/java/com/samskivert/depot/impl/jdbc/MySQLLiaison.java @@ -0,0 +1,145 @@ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2012 Michael Bayne, et al. +// http://github.com/samskivert/samskivert/blob/master/COPYING + +package com.samskivert.depot.impl.jdbc; + +import java.sql.*; +import java.util.List; + +/** + * A database liaison for the MySQL database. + */ +public class MySQLLiaison extends BaseLiaison +{ + @Override // from DatabaseLiaison + public boolean matchesURL (String url) + { + return url.startsWith("jdbc:mysql"); + } + + @Override // from DatabaseLiaison + public boolean isDuplicateRowException (SQLException sqe) + { + String msg = sqe.getMessage(); + return (msg != null && msg.indexOf("Duplicate entry") != -1); + } + + @Override // from DatabaseLiaison + public boolean isTransientException (SQLException sqe) + { + String msg = sqe.getMessage(); + return (msg != null && (msg.indexOf("Lost connection") != -1 || + msg.indexOf("link failure") != -1 || + msg.indexOf("Broken pipe") != -1)); + } + + @Override // from DatabaseLiaison + public void createGenerator (Connection conn, String tableName, String columnName, int initValue) + throws SQLException + { + // TODO: is there any way we can set the initial AUTO_INCREMENT value? + } + + @Override // from DatabaseLiaison + public void deleteGenerator (Connection conn, String tableName, String columnName) + throws SQLException + { + // AUTO_INCREMENT does not create any database entities that we need to delete + } + + @Override // from BaseLiaison + public int lastInsertedId (Connection conn, Statement istmt, String table, String column) + throws SQLException + { + // MySQL uses "GENERATED_KEY" as the column name for the last inserted key, so we have to + // hackily pass that to our super method to get things to work + return super.lastInsertedId(conn, istmt, table, "GENERATED_KEY"); + } + + @Override + protected int fetchLastInsertedId (Connection conn, String table, String column) + throws SQLException + { + // MySQL does not keep track of per-table-and-column insertion data, so we are pretty much + // going on blind faith here that we're fetching the right ID. In the overwhelming number + // of cases that will be so, but it's still not pretty. + Statement stmt = null; + try { + stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("select LAST_INSERT_ID()"); + return rs.next() ? rs.getInt(1) : super.fetchLastInsertedId(conn, table, column); + } finally { + JDBCUtil.close(stmt); + } + } + + @Override // from BaseLiaison + public boolean addIndexToTable (Connection conn, String table, List columns, + String ixName, boolean unique) throws SQLException + { + if (tableContainsIndex(conn, table, ixName)) { + return false; + } + + // MySQL's "CREATE INDEX" is buggy, it actually changes the case of the table names you're + // working with. Luckily (?) ALTER TABLE ADD INDEX works, so we do that here. + StringBuilder update = new StringBuilder("ALTER TABLE ").append(table).append(" ADD "); + if (unique) { + update.append("UNIQUE "); + } + update.append("INDEX ").append(indexSQL(ixName)).append(" ("); + appendColumns(columns, update); + update.append(")"); + + executeQuery(conn, update.toString()); + log("Database index '" + ixName + "' added to table '" + table + "'"); + return true; + } + + @Override // from BaseLiaison + public void dropIndex (Connection conn, String table, String index) throws SQLException + { + executeQuery(conn, "ALTER TABLE " + tableSQL(table) + " DROP INDEX " + columnSQL(index)); + } + + @Override // from BaseLiaison + public void dropPrimaryKey (Connection conn, String table, String pkName) throws SQLException + { + executeQuery(conn, "ALTER TABLE " + tableSQL(table) + " DROP PRIMARY KEY"); + } + + @Override // from BaseLiaison + public boolean renameColumn (Connection conn, String table, String oldColumnName, + String newColumnName, ColumnDefinition newColumnDef) + throws SQLException + { + if (!tableContainsColumn(conn, table, oldColumnName)) { + return false; + } + executeQuery(conn, "ALTER TABLE " + table + " CHANGE " + oldColumnName + " " + + newColumnName + " " + expandDefinition(newColumnDef)); + log("Renamed column '" + oldColumnName + "' on table '" + table + "' to '" + + newColumnName + "'"); + return true; + } + + @Override // from DatabaseLiaison + public String columnSQL (String column) + { + return "`" + column + "`"; + } + + @Override // from DatabaseLiaison + public String tableSQL (String table) + { + return "`" + table + "`"; + } + + @Override // from DatabaseLiaison + public String indexSQL (String index) + { + return "`" + index + "`"; + } +} diff --git a/src/main/java/com/samskivert/depot/impl/jdbc/PostgreSQLLiaison.java b/src/main/java/com/samskivert/depot/impl/jdbc/PostgreSQLLiaison.java new file mode 100644 index 0000000..a1eda57 --- /dev/null +++ b/src/main/java/com/samskivert/depot/impl/jdbc/PostgreSQLLiaison.java @@ -0,0 +1,139 @@ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2012 Michael Bayne, et al. +// http://github.com/samskivert/samskivert/blob/master/COPYING + +package com.samskivert.depot.impl.jdbc; + +import java.sql.*; + +/** + * A database liaison for the MySQL database. + */ +public class PostgreSQLLiaison extends BaseLiaison +{ + // from DatabaseLiaison + public boolean matchesURL (String url) + { + return url.startsWith("jdbc:postgresql"); + } + + // from DatabaseLiaison + public boolean isDuplicateRowException (SQLException sqe) + { + String msg = sqe.getMessage(); + return (msg != null && msg.indexOf("duplicate key") != -1); + } + + // from DatabaseLiaison + public boolean isTransientException (SQLException sqe) + { + // TODO: Add more error messages here as we encounter them. + String msg = sqe.getMessage(); + return (msg != null && + msg.indexOf("An I/O error occured while sending to the backend") != -1); + } + + @Override + protected int fetchLastInsertedId (Connection conn, String table, String column) + throws SQLException + { + // PostgreSQL's support for auto-generated ID's comes in the form of appropriately named + // sequences and DEFAULT nextval(sequence) modifiers in the ID columns. To get the next ID, + // we use the currval() method which is set in a database sessions when any given sequence + // is incremented. + Statement stmt = conn.createStatement(); + try { + ResultSet rs = stmt.executeQuery( + "select currval('\"" + table + "_" + column + "_seq\"')"); + return rs.next() ? rs.getInt(1) : super.fetchLastInsertedId(conn, table, column); + } finally { + JDBCUtil.close(stmt); + } + } + + // from DatabaseLiaison + public void createGenerator (Connection conn, String tableName, String columnName, int initValue) + throws SQLException + { + if (initValue == 1) { + return; // that's the default! yay, do nothing + } + + String seqname = "\"" + tableName + "_" + columnName + "_seq\""; + Statement stmt = conn.createStatement(); + try { + stmt.executeQuery("select setval('" + seqname + "', " + initValue + ", false)"); + } finally { + JDBCUtil.close(stmt); + } + log("Initial value of " + seqname + " set to " + initValue + "."); + } + + // from DatabaseLiaison + public void deleteGenerator (Connection conn, String table, String column) + throws SQLException + { + executeQuery(conn, "drop sequence if exists \"" + table + "_" + column + "_seq\""); + } + + @Override // from DatabaseLiaison + public boolean changeColumn (Connection conn, String table, String column, String type, + Boolean nullable, Boolean unique, String defaultValue) + throws SQLException + { + StringBuilder lbuf = new StringBuilder(); + if (type != null) { + executeQuery( + conn, "ALTER TABLE " + tableSQL(table) + " ALTER COLUMN " + columnSQL(column) + + " TYPE " + type); + lbuf.append("type=").append(type); + } + if (nullable != null) { + executeQuery( + conn, "ALTER TABLE " + tableSQL(table) + " ALTER COLUMN " + columnSQL(column) + + " " + (nullable ? "DROP NOT NULL" : "SET NOT NULL")); + if (lbuf.length() > 0) { + lbuf.append(", "); + } + lbuf.append("nullable=").append(nullable); + } + if (unique != null) { + // TODO: I think this requires ALTER TABLE DROP CONSTRAINT and so on + if (lbuf.length() > 0) { + lbuf.append(", "); + } + lbuf.append("unique=").append(unique).append(" (not implemented yet)"); + } + if (defaultValue != null) { + executeQuery( + conn, "ALTER TABLE " + tableSQL(table) + " ALTER COLUMN " + columnSQL(column) + + " " + (defaultValue.length() > 0 ? "SET DEFAULT " + defaultValue : "DROP DEFAULT")); + if (lbuf.length() > 0) { + lbuf.append(", "); + } + lbuf.append("defaultValue=").append(defaultValue); + } + log("Database column '" + column + "' of table '" + table + "' modified to have " + + "definition [" + lbuf + "]."); + return true; + } + + // from DatabaseLiaison + public String columnSQL (String column) + { + return "\"" + column + "\""; + } + + // from DatabaseLiaison + public String tableSQL (String table) + { + return "\"" + table + "\""; + } + + // from DatabaseLiaison + public String indexSQL (String index) + { + return "\"" + index + "\""; + } +} diff --git a/src/main/java/com/samskivert/depot/impl/operator/Div.java b/src/main/java/com/samskivert/depot/impl/operator/Div.java index 31def86..dced3b6 100644 --- a/src/main/java/com/samskivert/depot/impl/operator/Div.java +++ b/src/main/java/com/samskivert/depot/impl/operator/Div.java @@ -4,7 +4,7 @@ package com.samskivert.depot.impl.operator; -import com.samskivert.util.StringUtil; +import java.util.Arrays; import com.samskivert.depot.expression.SQLExpression; @@ -34,7 +34,7 @@ public class Div extends Arithmetic { for (int ii = 1; ii < operands.length; ii ++) { if (Double.valueOf(0).equals(NUMERICAL.apply(operands[ii]))) { - return new NoValue("Division by zero in: " + StringUtil.toString(operands)); + return new NoValue("Division by zero in: " + Arrays.toString(operands)); } } return evaluate(operands, "/", new Accumulator() { diff --git a/src/main/java/com/samskivert/depot/operator/Case.java b/src/main/java/com/samskivert/depot/operator/Case.java index 1e96943..27ad26e 100644 --- a/src/main/java/com/samskivert/depot/operator/Case.java +++ b/src/main/java/com/samskivert/depot/operator/Case.java @@ -12,7 +12,7 @@ import com.google.common.collect.Lists; import com.samskivert.depot.PersistentRecord; import com.samskivert.depot.expression.SQLExpression; import com.samskivert.depot.impl.FragmentVisitor; -import com.samskivert.util.Tuple; +import com.samskivert.depot.util.Tuple2; /** * The SQL 'case' operator. @@ -24,12 +24,12 @@ public class Case { int i = 0; for (; i + 1 < exps.length; i += 2) { - _whenExps.add(Tuple., SQLExpression>newTuple(exps[i], exps[i + 1])); + _whenExps.add(Tuple2., SQLExpression>create(exps[i], exps[i + 1])); } _elseExp = (i < exps.length) ? exps[i] : null; } - public List, SQLExpression>> getWhenExps () + public List, SQLExpression>> getWhenExps () { return _whenExps; } @@ -48,9 +48,9 @@ public class Case // from SQLExpression public void addClasses (Collection> classSet) { - for (Tuple, SQLExpression> tuple : _whenExps) { - tuple.left.addClasses(classSet); - tuple.right.addClasses(classSet); + for (Tuple2, SQLExpression> tuple : _whenExps) { + tuple.a.addClasses(classSet); + tuple.b.addClasses(classSet); } if (_elseExp != null) { _elseExp.addClasses(classSet); @@ -62,9 +62,9 @@ public class Case { StringBuilder builder = new StringBuilder(); builder.append("Case("); - for (Tuple, SQLExpression> tuple : _whenExps) { - builder.append(tuple.left.toString()).append("->"); - builder.append(tuple.right.toString()).append(","); + for (Tuple2, SQLExpression> tuple : _whenExps) { + builder.append(tuple.a.toString()).append("->"); + builder.append(tuple.b.toString()).append(","); } if (_elseExp != null) { builder.append(_elseExp.toString()).append(")"); @@ -72,6 +72,6 @@ public class Case return builder.toString(); } - protected List, SQLExpression>> _whenExps = Lists.newArrayList(); + protected List, SQLExpression>> _whenExps = Lists.newArrayList(); protected SQLExpression _elseExp; } diff --git a/src/main/java/com/samskivert/depot/tools/GenRecord.java b/src/main/java/com/samskivert/depot/tools/GenRecord.java index aaa52cb..df1e225 100644 --- a/src/main/java/com/samskivert/depot/tools/GenRecord.java +++ b/src/main/java/com/samskivert/depot/tools/GenRecord.java @@ -4,31 +4,25 @@ package com.samskivert.depot.tools; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; +import java.io.*; import java.lang.annotation.Annotation; -import java.lang.reflect.Field; -import java.lang.reflect.GenericArrayType; -import java.lang.reflect.Modifier; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; import java.nio.charset.Charset; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.lang.reflect.*; + import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; +import com.google.common.io.CharStreams; import com.google.common.io.Files; import com.samskivert.depot.PersistentRecord; @@ -36,10 +30,6 @@ import com.samskivert.depot.annotation.GeneratedValue; import com.samskivert.depot.annotation.Id; import com.samskivert.depot.annotation.Transient; import com.samskivert.depot.impl.DepotUtil; -import com.samskivert.io.StreamUtil; -import com.samskivert.util.ClassUtil; -import com.samskivert.util.GenUtil; -import com.samskivert.util.StringUtil; public abstract class GenRecord { @@ -95,9 +85,9 @@ public abstract class GenRecord List kflist = Lists.newArrayList(); if (!Modifier.isAbstract(rclass.getModifiers())) { // determine which fields make up our primary key; we'd just use Class.getFields() but - // that returns things in a random order whereas ClassUtil returns fields in - // declaration order starting from the top-most class and going down the line - for (Field field : ClassUtil.getFields(rclass)) { + // that returns things in a random order whereas ours returns fields in declaration + // order starting from the top-most class and going down the line + for (Field field : getFields(rclass)) { if (hasAnnotation(field, Id.class)) kflist.add(field); else if (hasAnnotation(field, GeneratedValue.class)) { logWarn("Skipping " + rclass.getName() + ". Field '" + field.getName() + @@ -221,7 +211,7 @@ public abstract class GenRecord Map fsubs = Maps.newHashMap(subs); fsubs.put("type", getTypeName(f.getGenericType())); fsubs.put("field", fname); - fsubs.put("capfield", StringUtil.unStudlyName(fname).toUpperCase()); + fsubs.put("capfield", unStudlyName(fname).toUpperCase()); // now generate our bits fsection.append(mergeTemplate(COL_TMPL, fsubs)); @@ -242,9 +232,9 @@ public abstract class GenRecord fieldNameList.append(", "); } String name = keyField.getName(); - argList.append(GenUtil.simpleName(keyField)).append(" ").append(name); + argList.append(simpleName(keyField.getGenericType())).append(" ").append(name); argNameList.append(name); - fieldNameList.append(StringUtil.unStudlyName(name)); + fieldNameList.append(unStudlyName(name)); } subs.put("argList", argList.toString()); @@ -264,31 +254,23 @@ public abstract class GenRecord if (fsection.length() > 0) { String prev = get(lines, nstart-1); - if (!StringUtil.isBlank(prev) && !prev.equals("{")) { - pout.println(); - } + if (!isBlank(prev) && !prev.equals("{")) pout.println(); pout.println(" " + FIELDS_START); pout.write(fsection.toString()); pout.println(" " + FIELDS_END); - if (!StringUtil.isBlank(get(lines, nend))) { - pout.println(); - } + if (!isBlank(get(lines, nend))) pout.println(); } for (int ii = nend; ii < mstart; ii++) { pout.println(lines[ii]); } if (msection.length() > 0) { - if (!StringUtil.isBlank(get(lines, mstart-1))) { - pout.println(); - } + if (!isBlank(get(lines, mstart-1))) pout.println(); pout.println(" " + METHODS_START); pout.write(msection.toString()); pout.println(" " + METHODS_END); String next = get(lines, mend); - if (!StringUtil.isBlank(next) && !next.equals("}")) { - pout.println(); - } + if (!isBlank(next) && !next.equals("}")) pout.println(); } for (int ii = mend; ii < lines.length; ii++) { pout.println(lines[ii]); @@ -340,8 +322,8 @@ public abstract class GenRecord protected String mergeTemplate (String tmpl, Map subs) { try { - String text = StreamUtil.toString( - getClass().getClassLoader().getResourceAsStream(tmpl), "UTF-8"); + InputStream in = getClass().getClassLoader().getResourceAsStream(tmpl); + String text = CharStreams.toString(new InputStreamReader(in, "UTF-8")); text = text.replace("\n", System.getProperty("line.separator")); for (Map.Entry entry : subs.entrySet()) { text = text.replaceAll("@"+entry.getKey()+"@", entry.getValue()); @@ -434,6 +416,121 @@ public abstract class GenRecord } } + protected static Field[] getFields (Class clazz) + { + List list = Lists.newArrayList(); + getFields(clazz, list); + return list.toArray(new Field[list.size()]); + } + + protected static void getFields (Class clazz, List addTo) + { + // first get the fields of the superclass + Class pclazz = clazz.getSuperclass(); + if (pclazz != null && !pclazz.equals(Object.class)) getFields(pclazz, addTo); + + // then reflect on this class's declared fields + Field[] fields = clazz.getDeclaredFields(); + + // override the default accessibility check for the fields + try { + AccessibleObject.setAccessible(fields, true); + } catch (SecurityException se) { + // ah well, only publics for us + } + + for (Field field : fields) { + int mods = field.getModifiers(); + // skip static and transient fields + if (Modifier.isStatic(mods) || Modifier.isTransient(mods)) continue; + addTo.add(field); + } + } + + protected static String simpleName (Type type) + { + if (type instanceof GenericArrayType) { + return simpleName(((GenericArrayType)type).getGenericComponentType()) + "[]"; + } else if (type instanceof Class) { + Class clazz = (Class)type; + if (clazz.isArray()) { + return simpleName(clazz.getComponentType()) + "[]"; + } else { + Package pkg = clazz.getPackage(); + int offset = (pkg == null) ? 0 : pkg.getName().length()+1; + return clazz.getName().substring(offset).replace('$', '.'); + } + + } else if (type instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType)type; + StringBuilder buf = new StringBuilder(); + for (Type arg : pt.getActualTypeArguments()) { + if (buf.length() > 0) { + buf.append(", "); + } + buf.append(simpleName(arg)); + } + return simpleName(pt.getRawType()) + "<" + buf + ">"; + + } else if (type instanceof WildcardType) { + WildcardType wt = (WildcardType)type; + if (wt.getLowerBounds().length > 0) { + String errmsg = "Generation of simple name for wildcard type with lower bounds " + + "not implemented [type=" + type + + ", lbounds=" + Arrays.toString(wt.getLowerBounds()) + "]"; + throw new IllegalArgumentException(errmsg); + } + if (wt.getUpperBounds().length > 1) { + String errmsg = "Generation of simple name for wildcard type with multiple upper " + + "bounds not implemented [type=" + type + + ", ubounds=" + Arrays.toString(wt.getUpperBounds()) + "]"; + throw new IllegalArgumentException(errmsg); + } + StringBuilder buf = new StringBuilder("?"); + if (!Object.class.equals(wt.getUpperBounds()[0])) { + buf.append(" extends ").append(simpleName(wt.getUpperBounds()[0])); + } + return buf.toString(); + + } else if (type instanceof TypeVariable) { + return ((TypeVariable)type).getName(); + + } else { + throw new IllegalArgumentException("Can't generate simple name [type=" + type + "]"); + } + } + + protected static boolean isBlank (String value) + { + for (int ii = 0, ll = (value == null) ? 0 : value.length(); ii < ll; ii++) { + if (!Character.isWhitespace(value.charAt(ii))) return false; + } + return true; + } + + protected static String unStudlyName (String name) + { + boolean seenLower = false; + StringBuilder nname = new StringBuilder(); + int nlen = name.length(); + for (int i = 0; i < nlen; i++) { + char c = name.charAt(i); + // if we see an upper case character and we've seen a lower case character since the + // last time we did so, slip in an _ + if (Character.isUpperCase(c)) { + if (seenLower) { + nname.append("_"); + } + seenLower = false; + nname.append(c); + } else { + seenLower = true; + nname.append(Character.toUpperCase(c)); + } + } + return nname.toString(); + } + /** Used to do our own classpath business. */ protected final ClassLoader _cloader; diff --git a/src/main/java/com/samskivert/depot/util/ByteEnum.java b/src/main/java/com/samskivert/depot/util/ByteEnum.java new file mode 100644 index 0000000..e1fbfff --- /dev/null +++ b/src/main/java/com/samskivert/depot/util/ByteEnum.java @@ -0,0 +1,82 @@ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2012 Michael Bayne, et al. +// http://github.com/samskivert/samskivert/blob/master/COPYING + +package com.samskivert.depot.util; + +import java.util.EnumSet; +import java.util.Set; + +/** + * An interface implemented by enums that can map themselves to a byte. See {@link ByteEnum.Util} + * for mapping back from a byte to an enum. + */ +public interface ByteEnum +{ + /** {@link ByteEnum} utility methods. */ + public static class Util + { + /** + * Returns the enum value with the specified code in the supplied enum class. + * + * @exception IllegalArgumentException thrown if the enum lacks a value that maps to the + * supplied code. + */ + public static & ByteEnum> E fromByte (Class eclass, byte code) { + for (E value : eclass.getEnumConstants()) { + if (value.toByte() == code) { + return value; + } + } + throw new IllegalArgumentException(eclass + " has no value with code " + code); + } + + /** + * Convert a Set of ByteEnums into an integer compactly representing the elements that are + * included. + */ + public static & ByteEnum> int setToInt (Set set) { + int flags = 0; + for (E value : set) { + flags |= toIntFlag(value); + } + return flags; + } + + /** + * Convert an int representation of ByteEnum flags into an EnumSet. + */ + public static & ByteEnum> EnumSet intToSet ( + Class eclass, int flags) { + EnumSet set = EnumSet.noneOf(eclass); + for (E value : eclass.getEnumConstants()) { + if ((flags & toIntFlag(value)) != 0) { + set.add(value); + } + } + return set; + } + + /** + * A helper function for setToInt() and intToSet() that validates that the specified + * ByteEnum value is not null and has a code between 0 and 31, inclusive. + */ + protected static & ByteEnum> int toIntFlag (E value) { + byte code = value.toByte(); // allow this to throw NPE + if (code < 0 || code > 31) { + throw new IllegalArgumentException( + "ByteEnum code is outside the range that can be turned into an int " + + "[value=" + value + ", code=" + code + "]"); + } + return (1 << code); + } + + // TODO: setToByteArray() and byteArrayToSet(), for larger ByteEnums? + } + + /** + * Returns the byte value to which to map this enum value. + */ + public byte toByte (); +} diff --git a/src/main/java/com/samskivert/depot/util/RuntimeUtil.java b/src/main/java/com/samskivert/depot/util/RuntimeUtil.java index 6f50325..31bebbf 100644 --- a/src/main/java/com/samskivert/depot/util/RuntimeUtil.java +++ b/src/main/java/com/samskivert/depot/util/RuntimeUtil.java @@ -17,7 +17,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.samskivert.depot.PersistentRecord; -import com.samskivert.util.Tuple; +import com.samskivert.depot.util.Tuple2; import static com.google.common.base.Preconditions.checkArgument; @@ -263,14 +263,14 @@ public class RuntimeUtil protected static Function getconv (Class fc, Class tc) { - return _converters.get(Tuple.newTuple(fc, tc)); + return _converters.get(Tuple2.create(fc, tc)); } protected static void regconv (Class fc, Class tc, Function conv) { @SuppressWarnings("unchecked") Function value = (Function)conv; - _converters.put(Tuple.newTuple(fc, tc), value); + _converters.put(Tuple2.create(fc, tc), value); } protected static Map> _converters = diff --git a/src/test/java/com/samskivert/depot/ByteEnumTest.java b/src/test/java/com/samskivert/depot/ByteEnumTest.java index 9bad277..288d7ac 100644 --- a/src/test/java/com/samskivert/depot/ByteEnumTest.java +++ b/src/test/java/com/samskivert/depot/ByteEnumTest.java @@ -9,9 +9,8 @@ import java.lang.reflect.Field; import org.junit.Test; import static org.junit.Assert.*; -import com.samskivert.util.ByteEnum; - import com.samskivert.depot.impl.FieldMarshaller; +import com.samskivert.depot.util.ByteEnum; /** * Tests ByteEnum related bits. diff --git a/src/test/java/com/samskivert/depot/MigrationTest.java b/src/test/java/com/samskivert/depot/MigrationTest.java index 5f445fd..5354348 100644 --- a/src/test/java/com/samskivert/depot/MigrationTest.java +++ b/src/test/java/com/samskivert/depot/MigrationTest.java @@ -11,7 +11,7 @@ import org.junit.Test; import com.samskivert.depot.annotation.Id; import com.samskivert.depot.impl.Modifier; -import com.samskivert.jdbc.DatabaseLiaison; +import com.samskivert.depot.impl.jdbc.DatabaseLiaison; /** * Tests various migrations. diff --git a/src/test/java/com/samskivert/depot/TestBase.java b/src/test/java/com/samskivert/depot/TestBase.java index aeed37c..9fd3189 100644 --- a/src/test/java/com/samskivert/depot/TestBase.java +++ b/src/test/java/com/samskivert/depot/TestBase.java @@ -12,8 +12,8 @@ import java.util.Properties; import com.google.common.collect.Lists; -import com.samskivert.jdbc.ConnectionProvider; -import com.samskivert.jdbc.StaticConnectionProvider; +import com.samskivert.depot.ConnectionProvider; +import com.samskivert.depot.StaticConnectionProvider; import com.samskivert.util.Calendars; import static org.junit.Assert.*; diff --git a/src/test/java/com/samskivert/depot/TransformTest.java b/src/test/java/com/samskivert/depot/TransformTest.java index 9f7b699..d0ddc42 100644 --- a/src/test/java/com/samskivert/depot/TransformTest.java +++ b/src/test/java/com/samskivert/depot/TransformTest.java @@ -16,15 +16,13 @@ import com.google.common.base.Objects; import com.google.common.collect.Lists; import com.google.common.collect.Sets; -import com.samskivert.util.ByteEnum; - import com.samskivert.depot.Key; -import com.samskivert.depot.expression.ColumnExp; - import com.samskivert.depot.annotation.Column; import com.samskivert.depot.annotation.Id; import com.samskivert.depot.annotation.Transform; +import com.samskivert.depot.expression.ColumnExp; import com.samskivert.depot.impl.FieldMarshaller; +import com.samskivert.depot.util.ByteEnum; import static org.junit.Assert.*; diff --git a/src/test/java/com/samskivert/depot/TransformersTest.java b/src/test/java/com/samskivert/depot/TransformersTest.java index e38e010..c691143 100644 --- a/src/test/java/com/samskivert/depot/TransformersTest.java +++ b/src/test/java/com/samskivert/depot/TransformersTest.java @@ -12,7 +12,7 @@ import static org.junit.Assert.*; import com.google.common.collect.Sets; -import com.samskivert.util.ByteEnum; +import com.samskivert.depot.util.ByteEnum; /** * Tests the stock transformers.