Author: bayard Date: Fri Aug 12 04:45:25 2011 New Revision: 1156964 URL: http://svn.apache.org/viewvc?rev=1156964&view=rev Log: Adding asynchronous query runner patch from William Speirs. Also moves DbUtils to Java6 dependent. DBUTILS-78
Added: commons/proper/dbutils/trunk/src/java/org/apache/commons/dbutils/AsyncQueryRunner.java (with props) commons/proper/dbutils/trunk/src/test/org/apache/commons/dbutils/AsyncQueryRunnerTest.java (with props) Modified: commons/proper/dbutils/trunk/pom.xml Modified: commons/proper/dbutils/trunk/pom.xml URL: http://svn.apache.org/viewvc/commons/proper/dbutils/trunk/pom.xml?rev=1156964&r1=1156963&r2=1156964&view=diff ============================================================================== --- commons/proper/dbutils/trunk/pom.xml [utf-8] (original) +++ commons/proper/dbutils/trunk/pom.xml [utf-8] Fri Aug 12 04:45:25 2011 @@ -173,14 +173,26 @@ <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> - <version>3.8.2</version> + <version>4.8.2</version> <scope>test</scope> </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>1.8.5</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-all</artifactId> + <version>1.1</version> + <scope>test</scope> + </dependency> </dependencies> <properties> - <maven.compile.source>1.5</maven.compile.source> - <maven.compile.target>1.5</maven.compile.target> + <maven.compile.source>1.6</maven.compile.source> + <maven.compile.target>1.6</maven.compile.target> <commons.componentid>dbutils</commons.componentid> <commons.release.version>1.3</commons.release.version> <commons.rc.version>RC4</commons.rc.version> @@ -205,8 +217,8 @@ <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> - <source>1.5</source> - <target>1.5</target> + <source>1.6</source> + <target>1.6</target> </configuration> </plugin> <plugin> @@ -268,7 +280,7 @@ <artifactId>maven-pmd-plugin</artifactId> <version>2.3</version> <configuration> - <targetJdk>1.5</targetJdk> + <targetJdk>1.6</targetJdk> </configuration> </plugin> <plugin> Added: commons/proper/dbutils/trunk/src/java/org/apache/commons/dbutils/AsyncQueryRunner.java URL: http://svn.apache.org/viewvc/commons/proper/dbutils/trunk/src/java/org/apache/commons/dbutils/AsyncQueryRunner.java?rev=1156964&view=auto ============================================================================== --- commons/proper/dbutils/trunk/src/java/org/apache/commons/dbutils/AsyncQueryRunner.java (added) +++ commons/proper/dbutils/trunk/src/java/org/apache/commons/dbutils/AsyncQueryRunner.java Fri Aug 12 04:45:25 2011 @@ -0,0 +1,786 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.dbutils; + +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; +import java.util.concurrent.RunnableFuture; + +import javax.sql.DataSource; + +/** + * Executes SQL queries with pluggable strategies for handling + * <code>ResultSet</code>s. This class is thread safe. + * + * @see ResultSetHandler + */ +public class AsyncQueryRunner { + + /** + * Is {@link ParameterMetaData#getParameterType(int)} broken (have we tried it yet)? + */ + private volatile boolean pmdKnownBroken = false; + + /** + * The DataSource to retrieve connections from. + */ + protected final DataSource ds; + + /** + * Constructor for QueryRunner. + */ + public AsyncQueryRunner() { + this(null, false); + } + + /** + * Constructor for QueryRunner, allows workaround for Oracle drivers + * @param pmdKnownBroken Oracle drivers don't support {@link ParameterMetaData#getParameterType(int) }; + * if <code>pmdKnownBroken</code> is set to true, we won't even try it; if false, we'll try it, + * and if it breaks, we'll remember not to use it again. + */ + public AsyncQueryRunner(boolean pmdKnownBroken) { + this(null, pmdKnownBroken); + } + + /** + * Constructor for QueryRunner, allows workaround for Oracle drivers. Methods that do not take a + * <code>Connection</code> parameter will retrieve connections from this + * <code>DataSource</code>. + * + * @param ds The <code>DataSource</code> to retrieve connections from. + */ + public AsyncQueryRunner(DataSource ds) { + this(ds, false); + } + + /** + * Constructor for QueryRunner, allows workaround for Oracle drivers. Methods that do not take a + * <code>Connection</code> parameter will retrieve connections from this + * <code>DataSource</code>. + * + * @param ds The <code>DataSource</code> to retrieve connections from. + * @param pmdKnownBroken Oracle drivers don't support {@link ParameterMetaData#getParameterType(int) }; + * if <code>pmdKnownBroken</code> is set to true, we won't even try it; if false, we'll try it, + * and if it breaks, we'll remember not to use it again. + */ + public AsyncQueryRunner(DataSource ds, boolean pmdKnownBroken) { + this.pmdKnownBroken = pmdKnownBroken; + this.ds = ds; + } + + protected class BatchCallableStatement implements Callable<int[]> { + private String sql; + private Object[][] params; + private Connection conn; + private boolean closeConn; + private PreparedStatement ps; + + public BatchCallableStatement(String sql, Object[][] params, Connection conn, boolean closeConn, PreparedStatement ps) { + this.sql = sql; + this.params = params; + this.conn = conn; + this.closeConn = closeConn; + this.ps = ps; + } + + public int[] call() throws Exception { + int[] ret = null; + + try { + ret = ps.executeBatch(); + } catch(SQLException e) { + rethrow(e, sql, (Object[])params); + } finally { + close(ps); + if(closeConn) + close(conn); + } + + return ret; + } + } + + /** + * Execute a batch of SQL INSERT, UPDATE, or DELETE queries. + * + * @param conn The Connection to use to run the query. The caller is + * responsible for closing this Connection. + * @param sql The SQL to execute. + * @param params An array of query replacement parameters. Each row in + * this array is one set of batch replacement values. + * @return A RunnableFuture which when completed returns the number of rows updated per statement. + * @throws SQLException if a database access error occurs + * @since DbUtils 1.1 + */ + public RunnableFuture<int[]> batch(Connection conn, String sql, Object[][] params) throws SQLException { + return this.batch(conn, false, sql, params); + } + + /** + * Execute a batch of SQL INSERT, UPDATE, or DELETE queries. The + * <code>Connection</code> is retrieved from the <code>DataSource</code> + * set in the constructor. This <code>Connection</code> must be in + * auto-commit mode or the update will not be saved. + * + * @param sql The SQL to execute. + * @param params An array of query replacement parameters. Each row in + * this array is one set of batch replacement values. + * @return A RunnableFuture which when completed returns the number of rows updated per statement. + * @throws SQLException if a database access error occurs + * @since DbUtils 1.1 + */ + public RunnableFuture<int[]> batch(String sql, Object[][] params) throws SQLException { + Connection conn = this.prepareConnection(); + + return this.batch(conn, true, sql, params); + } + + private RunnableFuture<int[]> batch(Connection conn, boolean closeConn, String sql, Object[][] params) throws SQLException { + if(conn == null) { + if(closeConn) + close(conn); + throw new SQLException("Null connection"); + } + + if(sql == null) { + if(closeConn) + close(conn); + throw new SQLException("Null SQL statement"); + } + + if(params == null) { + if(closeConn) + close(conn); + throw new SQLException("Null parameters. If parameters aren't need, pass an empty array."); + } + + PreparedStatement stmt = null; + FutureTask<int[]> ret = null; + try { + stmt = this.prepareStatement(conn, sql); + + for (int i = 0; i < params.length; i++) { + this.fillStatement(stmt, params[i]); + stmt.addBatch(); + } + + ret = new FutureTask<int[]>(new BatchCallableStatement(sql, params, conn, closeConn, stmt)); + + } catch (SQLException e) { + close(stmt); + close(conn); + this.rethrow(e, sql, (Object[])params); + } + + return ret; + } + + /** + * Fill the <code>PreparedStatement</code> replacement parameters with + * the given objects. + * @param stmt PreparedStatement to fill + * @param params Query replacement parameters; <code>null</code> is a valid + * value to pass in. + * @throws SQLException if a database access error occurs + */ + public void fillStatement(PreparedStatement stmt, Object... params) throws SQLException { + + // check the parameter count, if we can + ParameterMetaData pmd = null; + if (!pmdKnownBroken) { + pmd = stmt.getParameterMetaData(); + int stmtCount = pmd.getParameterCount(); + int paramsCount = params == null ? 0 : params.length; + + if (stmtCount != paramsCount) { + throw new SQLException("Wrong number of parameters: expected " + + stmtCount + ", was given " + paramsCount); + } + } + + // nothing to do here + if (params == null) { + return; + } + + for (int i = 0; i < params.length; i++) { + if (params[i] != null) { + stmt.setObject(i + 1, params[i]); + } else { + // VARCHAR works with many drivers regardless + // of the actual column type. Oddly, NULL and + // OTHER don't work with Oracle's drivers. + int sqlType = Types.VARCHAR; + if (!pmdKnownBroken) { + try { + sqlType = pmd.getParameterType(i + 1); + } catch (SQLException e) { + pmdKnownBroken = true; + } + } + stmt.setNull(i + 1, sqlType); + } + } + } + + /** + * Fill the <code>PreparedStatement</code> replacement parameters with the + * given object's bean property values. + * + * @param stmt + * PreparedStatement to fill + * @param bean + * a JavaBean object + * @param properties + * an ordered array of properties; this gives the order to insert + * values in the statement + * @throws SQLException + * if a database access error occurs + */ + public void fillStatementWithBean(PreparedStatement stmt, Object bean, + PropertyDescriptor[] properties) throws SQLException { + Object[] params = new Object[properties.length]; + for (int i = 0; i < properties.length; i++) { + PropertyDescriptor property = properties[i]; + Object value = null; + Method method = property.getReadMethod(); + if (method == null) { + throw new RuntimeException("No read method for bean property " + + bean.getClass() + " " + property.getName()); + } + try { + value = method.invoke(bean, new Object[0]); + } catch (InvocationTargetException e) { + throw new RuntimeException("Couldn't invoke method: " + method, e); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Couldn't invoke method with 0 arguments: " + method, e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Couldn't invoke method: " + method, e); + } + params[i] = value; + } + fillStatement(stmt, params); + } + + /** + * Fill the <code>PreparedStatement</code> replacement parameters with the + * given object's bean property values. + * + * @param stmt PreparedStatement to fill + * @param bean A JavaBean object + * @param propertyNames An ordered array of property names (these should match the + * getters/setters); this gives the order to insert values in the + * statement + * @throws SQLException If a database access error occurs + */ + public void fillStatementWithBean(PreparedStatement stmt, Object bean, String... propertyNames) throws SQLException { + PropertyDescriptor[] descriptors; + try { + descriptors = Introspector.getBeanInfo(bean.getClass()) + .getPropertyDescriptors(); + } catch (IntrospectionException e) { + throw new RuntimeException("Couldn't introspect bean " + bean.getClass().toString(), e); + } + PropertyDescriptor[] sorted = new PropertyDescriptor[propertyNames.length]; + for (int i = 0; i < propertyNames.length; i++) { + String propertyName = propertyNames[i]; + if (propertyName == null) { + throw new NullPointerException("propertyName can't be null: " + i); + } + boolean found = false; + for (int j = 0; j < descriptors.length; j++) { + PropertyDescriptor descriptor = descriptors[j]; + if (propertyName.equals(descriptor.getName())) { + sorted[i] = descriptor; + found = true; + break; + } + } + if (!found) { + throw new RuntimeException("Couldn't find bean property: " + + bean.getClass() + " " + propertyName); + } + } + fillStatementWithBean(stmt, bean, sorted); + } + + /** + * Returns the <code>DataSource</code> this runner is using. + * <code>QueryRunner</code> methods always call this method to get the + * <code>DataSource</code> so subclasses can provide specialized + * behavior. + * + * @return DataSource the runner is using + */ + public DataSource getDataSource() { + return this.ds; + } + + /** + * Factory method that creates and initializes a + * <code>PreparedStatement</code> object for the given SQL. + * <code>QueryRunner</code> methods always call this method to prepare + * statements for them. Subclasses can override this method to provide + * special PreparedStatement configuration if needed. This implementation + * simply calls <code>conn.prepareStatement(sql)</code>. + * + * @param conn The <code>Connection</code> used to create the + * <code>PreparedStatement</code> + * @param sql The SQL statement to prepare. + * @return An initialized <code>PreparedStatement</code>. + * @throws SQLException if a database access error occurs + */ + protected PreparedStatement prepareStatement(Connection conn, String sql) + throws SQLException { + + return conn.prepareStatement(sql); + } + + /** + * Factory method that creates and initializes a + * <code>Connection</code> object. <code>QueryRunner</code> methods + * always call this method to retrieve connections from its DataSource. + * Subclasses can override this method to provide + * special <code>Connection</code> configuration if needed. This + * implementation simply calls <code>ds.getConnection()</code>. + * + * @return An initialized <code>Connection</code>. + * @throws SQLException if a database access error occurs + * @since DbUtils 1.1 + */ + protected Connection prepareConnection() throws SQLException { + if(this.getDataSource() == null) { + throw new SQLException("QueryRunner requires a DataSource to be " + + "invoked in this way, or a Connection should be passed in"); + } + return this.getDataSource().getConnection(); + } + + protected class QueryCallableStatement<T> implements Callable<T> { + private String sql; + private Object[] params; + private Connection conn; + private boolean closeConn; + private PreparedStatement ps; + private ResultSetHandler<T> rsh; + + public QueryCallableStatement(Connection conn, boolean closeConn, PreparedStatement ps, ResultSetHandler<T> rsh, String sql, Object... params) { + this.sql = sql; + this.params = params; + this.conn = conn; + this.closeConn = closeConn; + this.ps = ps; + this.rsh = rsh; + } + + public T call() throws Exception { + ResultSet rs = null; + T ret = null; + + try { + rs = wrap(ps.executeQuery()); + ret = rsh.handle(rs); + } catch(SQLException e) { + rethrow(e, sql, params); + } finally { + try { + close(rs); + } finally { + close(ps); + if(closeConn) + close(conn); + } + } + + return ret; + } + + } + + private <T> RunnableFuture<T> query(Connection conn, boolean closeConn, String sql, ResultSetHandler<T> rsh, Object... params) throws SQLException { + PreparedStatement stmt = null; + FutureTask<T> ret = null; + + if(conn == null) { + if(closeConn) + close(conn); + throw new SQLException("Null connection"); + } + + if(sql == null) { + if(closeConn) + close(conn); + throw new SQLException("Null SQL statement"); + } + + if(rsh == null) { + if(closeConn) + close(conn); + throw new SQLException("Null ResultSetHandler"); + } + + try { + stmt = this.prepareStatement(conn, sql); + this.fillStatement(stmt, params); + + ret = new FutureTask<T>(new QueryCallableStatement<T>(conn, closeConn, stmt, rsh, sql, params)); + + } catch (SQLException e) { + close(stmt); + if(closeConn) + close(conn); + this.rethrow(e, sql, params); + } + + return ret; + } + + /** + * Execute an SQL SELECT query with replacement parameters. The + * caller is responsible for closing the connection. + * @param <T> The type of object that the handler returns + * @param conn The connection to execute the query in. + * @param sql The query to execute. + * @param rsh The handler that converts the results into an object. + * @param params The replacement parameters. + * @return A RunnableFuture which when completed returns the object returned by the handler. + * @throws SQLException if a database access error occurs + */ + public <T> RunnableFuture<T> query(Connection conn, String sql, ResultSetHandler<T> rsh, Object... params) throws SQLException { + return query(conn, false, sql, rsh, params); + } + + /** + * Execute an SQL SELECT query without any replacement parameters. The + * caller is responsible for closing the connection. + * @param <T> The type of object that the handler returns + * @param conn The connection to execute the query in. + * @param sql The query to execute. + * @param rsh The handler that converts the results into an object. + * @return A RunnableFuture which when completed returns the object returned by the handler. + * @throws SQLException if a database access error occurs + */ + public <T> RunnableFuture<T> query(Connection conn, String sql, ResultSetHandler<T> rsh) throws SQLException { + return this.query(conn, false, sql, rsh, (Object[]) null); + } + + /** + * Executes the given SELECT SQL query and returns a result object. + * The <code>Connection</code> is retrieved from the + * <code>DataSource</code> set in the constructor. + * @param <T> The type of object that the handler returns + * @param sql The SQL statement to execute. + * @param rsh The handler used to create the result object from + * the <code>ResultSet</code>. + * @param params Initialize the PreparedStatement's IN parameters with + * this array. + * @return A RunnableFuture which when completed returns the object generated by the handler. + * @throws SQLException if a database access error occurs + */ + public <T> RunnableFuture<T> query(String sql, ResultSetHandler<T> rsh, Object... params) throws SQLException { + Connection conn = this.prepareConnection(); + + return this.query(conn, true, sql, rsh, params); + } + + /** + * Executes the given SELECT SQL without any replacement parameters. + * The <code>Connection</code> is retrieved from the + * <code>DataSource</code> set in the constructor. + * @param <T> The type of object that the handler returns + * @param sql The SQL statement to execute. + * @param rsh The handler used to create the result object from + * the <code>ResultSet</code>. + * + * @return A RunnableFuture which when completed returns the object generated by the handler. + * @throws SQLException if a database access error occurs + */ + public <T> RunnableFuture<T> query(String sql, ResultSetHandler<T> rsh) throws SQLException { + Connection conn = this.prepareConnection(); + return this.query(conn, true, sql, rsh, (Object[]) null); + } + + /** + * Throws a new exception with a more informative error message. + * + * @param cause The original exception that will be chained to the new + * exception when it's rethrown. + * + * @param sql The query that was executing when the exception happened. + * + * @param params The query replacement parameters; <code>null</code> is a + * valid value to pass in. + * + * @throws SQLException if a database access error occurs + */ + protected void rethrow(SQLException cause, String sql, Object... params) + throws SQLException { + + String causeMessage = cause.getMessage(); + if (causeMessage == null) { + causeMessage = ""; + } + StringBuffer msg = new StringBuffer(causeMessage); + + msg.append(" Query: "); + msg.append(sql); + msg.append(" Parameters: "); + + if (params == null) { + msg.append("[]"); + } else { + msg.append(Arrays.deepToString(params)); + } + + SQLException e = new SQLException(msg.toString(), cause.getSQLState(), + cause.getErrorCode()); + e.setNextException(cause); + + throw e; + } + + protected class UpdateCallableStatement implements Callable<Integer> { + private String sql; + private Object[] params; + private Connection conn; + private boolean closeConn; + private PreparedStatement ps; + + public UpdateCallableStatement(Connection conn, boolean closeConn, PreparedStatement ps, String sql, Object... params) { + this.sql = sql; + this.params = params; + this.conn = conn; + this.closeConn = closeConn; + this.ps = ps; + } + + public Integer call() throws Exception { + int rows = 0; + + try { + rows = ps.executeUpdate(); + } catch (SQLException e) { + rethrow(e, sql, params); + } finally { + close(ps); + if(closeConn) + close(conn); + } + + return rows; + } + + } + + private RunnableFuture<Integer> update(Connection conn, boolean closeConn, String sql, Object... params) throws SQLException { + PreparedStatement stmt = null; + FutureTask<Integer> ret = null; + + if(conn == null) { + if(closeConn) + close(conn); + throw new SQLException("Null connection"); + } + + if(sql == null) { + if(closeConn) + close(conn); + throw new SQLException("Null SQL statement"); + } + + try { + stmt = this.prepareStatement(conn, sql); + this.fillStatement(stmt, params); + + ret = new FutureTask<Integer>(new UpdateCallableStatement(conn, closeConn, stmt, sql, params)); + + } catch (SQLException e) { + close(stmt); + if(closeConn) + close(conn); + this.rethrow(e, sql, params); + } + + return ret; + } + + /** + * Execute an SQL INSERT, UPDATE, or DELETE query without replacement + * parameters. + * + * @param conn The connection to use to run the query. + * @param sql The SQL to execute. + * @return A RunnableFuture which when completed returns the number of rows updated. + * @throws SQLException if a database access error occurs + */ + public RunnableFuture<Integer> update(Connection conn, String sql) throws SQLException { + return this.update(conn, false, sql, (Object[]) null); + } + + /** + * Execute an SQL INSERT, UPDATE, or DELETE query with a single replacement + * parameter. + * + * @param conn The connection to use to run the query. + * @param sql The SQL to execute. + * @param param The replacement parameter. + * @return A RunnableFuture which when completed returns the number of rows updated. + * @throws SQLException if a database access error occurs + */ + public RunnableFuture<Integer> update(Connection conn, String sql, Object param) throws SQLException { + return this.update(conn, false, sql, new Object[] { param }); + } + + /** + * Execute an SQL INSERT, UPDATE, or DELETE query. + * + * @param conn The connection to use to run the query. + * @param sql The SQL to execute. + * @param params The query replacement parameters. + * @return A RunnableFuture which when completed returns the number of rows updated. + * @throws SQLException if a database access error occurs + */ + public RunnableFuture<Integer> update(Connection conn, String sql, Object... params) throws SQLException { + return this.update(conn, false, sql, params); + } + + /** + * Executes the given INSERT, UPDATE, or DELETE SQL statement without + * any replacement parameters. The <code>Connection</code> is retrieved + * from the <code>DataSource</code> set in the constructor. This + * <code>Connection</code> must be in auto-commit mode or the update will + * not be saved. + * + * @param sql The SQL statement to execute. + * @throws SQLException if a database access error occurs + * @return A RunnableFuture which when completed returns the number of rows updated. + */ + public RunnableFuture<Integer> update(String sql) throws SQLException { + Connection conn = this.prepareConnection(); + return this.update(conn, true, sql, (Object[]) null); + } + + /** + * Executes the given INSERT, UPDATE, or DELETE SQL statement with + * a single replacement parameter. The <code>Connection</code> is + * retrieved from the <code>DataSource</code> set in the constructor. + * This <code>Connection</code> must be in auto-commit mode or the + * update will not be saved. + * + * @param sql The SQL statement to execute. + * @param param The replacement parameter. + * @throws SQLException if a database access error occurs + * @return A RunnableFuture which when completed returns the number of rows updated. + */ + public RunnableFuture<Integer> update(String sql, Object param) throws SQLException { + Connection conn = this.prepareConnection(); + return this.update(conn, true, sql, new Object[] { param }); + } + + /** + * Executes the given INSERT, UPDATE, or DELETE SQL statement. The + * <code>Connection</code> is retrieved from the <code>DataSource</code> + * set in the constructor. This <code>Connection</code> must be in + * auto-commit mode or the update will not be saved. + * + * @param sql The SQL statement to execute. + * @param params Initializes the PreparedStatement's IN (i.e. '?') + * parameters. + * @throws SQLException if a database access error occurs + * @return A RunnableFuture which when completed returns the number of rows updated. + */ + public RunnableFuture<Integer> update(String sql, Object... params) throws SQLException { + Connection conn = this.prepareConnection(); + return this.update(conn, true, sql, params); + } + + /** + * Wrap the <code>ResultSet</code> in a decorator before processing it. + * This implementation returns the <code>ResultSet</code> it is given + * without any decoration. + * + * <p> + * Often, the implementation of this method can be done in an anonymous + * inner class like this: + * </p> + * <pre> + * QueryRunner run = new QueryRunner() { + * protected ResultSet wrap(ResultSet rs) { + * return StringTrimmedResultSet.wrap(rs); + * } + * }; + * </pre> + * + * @param rs The <code>ResultSet</code> to decorate; never + * <code>null</code>. + * @return The <code>ResultSet</code> wrapped in some decorator. + */ + protected ResultSet wrap(ResultSet rs) { + return rs; + } + + /** + * Close a <code>Connection</code>. This implementation avoids closing if + * null and does <strong>not</strong> suppress any exceptions. Subclasses + * can override to provide special handling like logging. + * @param conn Connection to close + * @throws SQLException if a database access error occurs + * @since DbUtils 1.1 + */ + protected void close(Connection conn) throws SQLException { + DbUtils.close(conn); + } + + /** + * Close a <code>Statement</code>. This implementation avoids closing if + * null and does <strong>not</strong> suppress any exceptions. Subclasses + * can override to provide special handling like logging. + * @param stmt Statement to close + * @throws SQLException if a database access error occurs + * @since DbUtils 1.1 + */ + protected void close(Statement stmt) throws SQLException { + DbUtils.close(stmt); + } + + /** + * Close a <code>ResultSet</code>. This implementation avoids closing if + * null and does <strong>not</strong> suppress any exceptions. Subclasses + * can override to provide special handling like logging. + * @param rs ResultSet to close + * @throws SQLException if a database access error occurs + * @since DbUtils 1.1 + */ + protected void close(ResultSet rs) throws SQLException { + DbUtils.close(rs); + } + +} Propchange: commons/proper/dbutils/trunk/src/java/org/apache/commons/dbutils/AsyncQueryRunner.java ------------------------------------------------------------------------------ svn:eol-style = native Added: commons/proper/dbutils/trunk/src/test/org/apache/commons/dbutils/AsyncQueryRunnerTest.java URL: http://svn.apache.org/viewvc/commons/proper/dbutils/trunk/src/test/org/apache/commons/dbutils/AsyncQueryRunnerTest.java?rev=1156964&view=auto ============================================================================== --- commons/proper/dbutils/trunk/src/test/org/apache/commons/dbutils/AsyncQueryRunnerTest.java (added) +++ commons/proper/dbutils/trunk/src/test/org/apache/commons/dbutils/AsyncQueryRunnerTest.java Fri Aug 12 04:45:25 2011 @@ -0,0 +1,569 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.dbutils; + +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.RunnableFuture; + +import javax.sql.DataSource; + +import org.apache.commons.dbutils.handlers.ArrayHandler; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class AsyncQueryRunnerTest { + AsyncQueryRunner runner; + ArrayHandler handler; + + @Mock DataSource dataSource; + @Mock Connection conn; + @Mock PreparedStatement stmt; + @Mock ParameterMetaData meta; + @Mock ResultSet results; + + static final Method getParameterCount, getParameterType, getParameterMetaData; + static { + try { + getParameterCount = ParameterMetaData.class.getMethod("getParameterCount", new Class[0]); + getParameterType = ParameterMetaData.class.getMethod("getParameterType", new Class[]{int.class}); + getParameterMetaData = PreparedStatement.class.getMethod("getParameterMetaData", new Class[0]); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); // init the mocks + + when(dataSource.getConnection()).thenReturn(conn); + when(conn.prepareStatement(any(String.class))).thenReturn(stmt); + when(stmt.getParameterMetaData()).thenReturn(meta); + when(stmt.getResultSet()).thenReturn(results); + when(stmt.executeQuery()).thenReturn(results); + when(results.next()).thenReturn(false); + + handler = new ArrayHandler(); + runner = new AsyncQueryRunner(dataSource); + } + + // + // Batch test cases + // + + private void callGoodBatch(Connection conn, Object[][] params) throws Exception { + when(meta.getParameterCount()).thenReturn(2); + RunnableFuture<int[]> future = runner.batch(conn, "select * from blah where ? = ?", params); + verify(stmt, times(2)).addBatch(); + verify(stmt, never()).close(); // make sure the statement is still open + + future.run(); + verify(stmt, times(1)).executeBatch(); + verify(stmt, times(1)).close(); // make sure we closed the statement + verify(conn, times(0)).close(); // make sure we closed the connection + + future.get(); // make sure an exception wasn't thrown + } + + private void callGoodBatch(Object[][] params) throws Exception { + when(meta.getParameterCount()).thenReturn(2); + RunnableFuture<int[]> future = runner.batch("select * from blah where ? = ?", params); + verify(stmt, times(2)).addBatch(); + verify(stmt, never()).close(); // make sure the statement is still open + + future.run(); + verify(stmt, times(1)).executeBatch(); + verify(stmt, times(1)).close(); // make sure we closed the statement + verify(conn, times(1)).close(); // make sure we closed the connection + + future.get(); // make sure an exception wasn't thrown + } + + @Test + public void testGoodBatch() throws Exception { + String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; + + callGoodBatch(params); + } + + @Test + public void testGoodBatchPmdTrue() throws Exception { + runner = new AsyncQueryRunner(dataSource, true); + String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; + + callGoodBatch(params); + } + + @Test + public void testGoodBatchDefaultConstructor() throws Exception { + runner = new AsyncQueryRunner(); + String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; + + callGoodBatch(conn, params); + } + + @Test + public void testNullParamsBatch() throws Exception { + String[][] params = new String[][] { { null, "unit" }, { "test", null } }; + + callGoodBatch(params); + } + + + + // helper method for calling batch when an exception is expected + private void callBatchWithException(String sql, Object[][] params) throws Exception { + RunnableFuture<int[]> future = null; + boolean caught = false; + + try { + future = runner.batch(sql, params); + verify(stmt, times(2)).addBatch(); + verify(stmt, never()).close(); // make sure the statement is still open + + future.run(); + verify(stmt, times(1)).executeBatch(); + verify(stmt, times(1)).close(); // make sure the statement is closed + verify(conn, times(1)).close(); // make sure the connection is closed + + future.get(); + } catch(SQLException e) { + caught = true; + } catch(ExecutionException e) { + caught = true; + } + + if(!caught) + fail("Exception never thrown, but expected"); + } + + @Test + public void testTooFewParamsBatch() throws Exception { + String[][] params = new String[][] { { "unit" }, { "test" } }; + + callBatchWithException("select * from blah where ? = ?", params); + } + + @Test + public void testTooManyParamsBatch() throws Exception { + String[][] params = new String[][] { { "unit", "unit", "unit" }, { "test", "test", "test" } }; + + callBatchWithException("select * from blah where ? = ?", params); + } + + @Test(expected=SQLException.class) + public void testNullConnectionBatch() throws Exception { + String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; + + when(meta.getParameterCount()).thenReturn(2); + when(dataSource.getConnection()).thenReturn(null); + + runner.batch("select * from blah where ? = ?", params); + } + + @Test(expected=SQLException.class) + public void testNullSqlBatch() throws Exception { + String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; + + when(meta.getParameterCount()).thenReturn(2); + + runner.batch(null, params); + } + + @Test(expected=SQLException.class) + public void testNullParamsArgBatch() throws Exception { + when(meta.getParameterCount()).thenReturn(2); + + runner.batch("select * from blah where ? = ?", null); + } + + @Test + public void testAddBatchException() throws Exception { + String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; + + doThrow(new SQLException()).when(stmt).addBatch(); + + callBatchWithException("select * from blah where ? = ?", params); + } + + @Test + public void testExecuteBatchException() throws Exception { + String[][] params = new String[][] { { "unit", "unit" }, { "test", "test" } }; + + doThrow(new SQLException()).when(stmt).executeBatch(); + + callBatchWithException("select * from blah where ? = ?", params); + } + + + // + // Query test cases + // + private void callGoodQuery(Connection conn) throws Exception { + when(meta.getParameterCount()).thenReturn(2); + RunnableFuture<Object[]> future = runner.query(conn, "select * from blah where ? = ?", handler, "unit", "test"); + verify(stmt, never()).close(); // make sure the statement is still open + + future.run(); + verify(stmt, times(1)).executeQuery(); + verify(results, times(1)).close(); + verify(stmt, times(1)).close(); // make sure we closed the statement + verify(conn, times(0)).close(); // make sure we closed the connection + + future.get(); // make sure an exception wasn't thrown + + // call the other variation of query + when(meta.getParameterCount()).thenReturn(0); + future = runner.query(conn, "select * from blah", handler); + verify(stmt, times(1)).close(); // make sure the statement has only been closed once + + future.run(); + verify(stmt, times(2)).executeQuery(); + verify(results, times(2)).close(); + verify(stmt, times(2)).close(); // make sure we closed the statement + verify(conn, times(0)).close(); // make sure we closed the connection + + future.get(); // make sure an exception wasn't thrown + + } + + private void callGoodQuery() throws Exception { + when(meta.getParameterCount()).thenReturn(2); + RunnableFuture<Object[]> future = runner.query("select * from blah where ? = ?", handler, "unit", "test"); + verify(stmt, never()).close(); // make sure the statement is still open + + future.run(); + verify(stmt, times(1)).executeQuery(); + verify(results, times(1)).close(); + verify(stmt, times(1)).close(); // make sure we closed the statement + verify(conn, times(1)).close(); // make sure we closed the connection + + future.get(); // make sure an exception wasn't thrown + + // call the other variation of query + when(meta.getParameterCount()).thenReturn(0); + future = runner.query("select * from blah", handler); + verify(stmt, times(1)).close(); // make sure the statement is still open + + future.run(); + verify(stmt, times(2)).executeQuery(); + verify(results, times(2)).close(); + verify(stmt, times(2)).close(); // make sure we closed the statement + verify(conn, times(2)).close(); // make sure we closed the connection + + future.get(); // make sure an exception wasn't thrown + } + + @Test + public void testGoodQuery() throws Exception { + callGoodQuery(); + } + + @Test + public void testGoodQueryPmdTrue() throws Exception { + runner = new AsyncQueryRunner(true); + callGoodQuery(conn); + } + + @Test + public void testGoodQueryDefaultConstructor() throws Exception { + runner = new AsyncQueryRunner(); + callGoodQuery(conn); + } + + + // helper method for calling batch when an exception is expected + private void callQueryWithException(Object... params) throws Exception { + RunnableFuture<Object[]> future = null; + boolean caught = false; + + try { + when(meta.getParameterCount()).thenReturn(2); + future = runner.query("select * from blah where ? = ?", handler, params); + verify(stmt, never()).close(); // make sure the statement is still open + + future.run(); + verify(stmt, times(1)).executeQuery(); + verify(results, times(1)).close(); + verify(stmt, times(1)).close(); // make sure we closed the statement + verify(conn, times(1)).close(); // make sure we closed the connection + + future.get(); + } catch(SQLException e) { + caught = true; + } catch(ExecutionException e) { + caught = true; + } + + if(!caught) + fail("Exception never thrown, but expected"); + } + + @Test + public void testNoParamsQuery() throws Exception { + callQueryWithException(); + } + + @Test + public void testTooFewParamsQuery() throws Exception { + callQueryWithException("unit"); + } + + @Test + public void testTooManyParamsQuery() throws Exception { + callQueryWithException("unit", "test", "fail"); + } + + @Test(expected=SQLException.class) + public void testNullConnectionQuery() throws Exception { + when(meta.getParameterCount()).thenReturn(2); + when(dataSource.getConnection()).thenReturn(null); + + runner.query("select * from blah where ? = ?", handler, "unit", "test"); + } + + @Test(expected=SQLException.class) + public void testNullSqlQuery() throws Exception { + when(meta.getParameterCount()).thenReturn(2); + + runner.query(null, handler); + } + + @Test(expected=SQLException.class) + public void testNullHandlerQuery() throws Exception { + when(meta.getParameterCount()).thenReturn(2); + + runner.query("select * from blah where ? = ?", null); + } + + @Test + public void testExecuteQueryException() throws Exception { + doThrow(new SQLException()).when(stmt).executeQuery(); + + callQueryWithException(handler, "unit", "test"); + } + + + // + // Update test cases + // + private void callGoodUpdate(Connection conn) throws Exception { + when(meta.getParameterCount()).thenReturn(2); + RunnableFuture<Integer> future = runner.update(conn, "update blah set ? = ?", "unit", "test"); + verify(stmt, never()).close(); // make sure the statement is still open + + future.run(); + verify(stmt, times(1)).executeUpdate(); + verify(stmt, times(1)).close(); // make sure we closed the statement + verify(conn, times(0)).close(); // make sure we closed the connection + + future.get(); // make sure an exception wasn't thrown + + // call the other variation + when(meta.getParameterCount()).thenReturn(0); + future = runner.update(conn, "update blah set unit = test"); + verify(stmt, times(1)).close(); // make sure the statement is still open + + future.run(); + verify(stmt, times(2)).executeUpdate(); + verify(stmt, times(2)).close(); // make sure we closed the statement + verify(conn, times(0)).close(); // make sure we closed the connection + + future.get(); // make sure an exception wasn't thrown + + // call the other variation + when(meta.getParameterCount()).thenReturn(1); + future = runner.update(conn, "update blah set unit = ?", "test"); + verify(stmt, times(2)).close(); // make sure the statement is still open + + future.run(); + verify(stmt, times(3)).executeUpdate(); + verify(stmt, times(3)).close(); // make sure we closed the statement + verify(conn, times(0)).close(); // make sure we closed the connection + + future.get(); // make sure an exception wasn't thrown + } + + private void callGoodUpdate() throws Exception { + when(meta.getParameterCount()).thenReturn(2); + RunnableFuture<Integer> future = runner.update("update blah set ? = ?", "unit", "test"); + verify(stmt, never()).close(); // make sure the statement is still open + + future.run(); + verify(stmt, times(1)).executeUpdate(); + verify(stmt, times(1)).close(); // make sure we closed the statement + verify(conn, times(1)).close(); // make sure we closed the connection + + future.get(); // make sure an exception wasn't thrown + + // call the other variation + when(meta.getParameterCount()).thenReturn(0); + future = runner.update("update blah set unit = test"); + verify(stmt, times(1)).close(); // make sure the statement is still open + + future.run(); + verify(stmt, times(2)).executeUpdate(); + verify(stmt, times(2)).close(); // make sure we closed the statement + verify(conn, times(2)).close(); // make sure we closed the connection + + future.get(); // make sure an exception wasn't thrown + + // call the other variation + when(meta.getParameterCount()).thenReturn(1); + future = runner.update("update blah set unit = ?", "test"); + verify(stmt, times(2)).close(); // make sure the statement is still open + + future.run(); + verify(stmt, times(3)).executeUpdate(); + verify(stmt, times(3)).close(); // make sure we closed the statement + verify(conn, times(3)).close(); // make sure we closed the connection + + future.get(); // make sure an exception wasn't thrown + } + + @Test + public void testGoodUpdate() throws Exception { + callGoodUpdate(); + } + + @Test + public void testGoodUpdatePmdTrue() throws Exception { + runner = new AsyncQueryRunner(true); + callGoodUpdate(conn); + } + + @Test + public void testGoodUpdateDefaultConstructor() throws Exception { + runner = new AsyncQueryRunner(); + callGoodUpdate(conn); + } + + // helper method for calling batch when an exception is expected + private void callUpdateWithException(Object... params) throws Exception { + RunnableFuture<Integer> future = null; + boolean caught = false; + + try { + when(meta.getParameterCount()).thenReturn(2); + future = runner.update("select * from blah where ? = ?", params); + verify(stmt, never()).close(); // make sure the statement is still open + + future.run(); + verify(stmt, times(1)).executeUpdate(); + verify(stmt, times(1)).close(); // make sure we closed the statement + verify(conn, times(1)).close(); // make sure we closed the connection + + future.get(); + } catch(SQLException e) { + caught = true; + } catch(ExecutionException e) { + caught = true; + } + + if(!caught) + fail("Exception never thrown, but expected"); + } + + @Test + public void testNoParamsUpdate() throws Exception { + callUpdateWithException(); + } + + @Test + public void testTooFewParamsUpdate() throws Exception { + callUpdateWithException("unit"); + } + + @Test + public void testTooManyParamsUpdate() throws Exception { + callUpdateWithException("unit", "test", "fail"); + } + + @Test(expected=SQLException.class) + public void testNullConnectionUpdate() throws Exception { + when(meta.getParameterCount()).thenReturn(2); + when(dataSource.getConnection()).thenReturn(null); + + runner.update("select * from blah where ? = ?", "unit", "test"); + } + + @Test(expected=SQLException.class) + public void testNullSqlUpdate() throws Exception { + when(meta.getParameterCount()).thenReturn(2); + + runner.update(null); + } + + @Test + public void testExecuteUpdateException() throws Exception { + doThrow(new SQLException()).when(stmt).executeUpdate(); + + callUpdateWithException("unit", "test"); + } + + // + // Random tests + // + class MyBean { + private int a; + private double b; + private String c; + + public int getA() { return a; } + public void setA(int a) { this.a = a; } + public double getB() { return b; } + public void setB(double b) { this.b = b; } + public String getC() { return c; } + public void setC(String c) { this.c = c; } + } + + @Test + public void testFillStatementWithBean() throws Exception { + MyBean bean = new MyBean(); + when(meta.getParameterCount()).thenReturn(3); + runner.fillStatementWithBean(stmt, bean, new String[] { "a", "b", "c" }); + } + + @Test(expected=NullPointerException.class) + public void testFillStatementWithBeanNullNames() throws Exception { + MyBean bean = new MyBean(); + when(meta.getParameterCount()).thenReturn(3); + runner.fillStatementWithBean(stmt, bean, new String[] { "a", "b", null }); + } + + @Test(expected=SQLException.class) + public void testBadPrepareConnection() throws Exception { + runner = new AsyncQueryRunner(); + runner.update("update blah set unit = test"); + } +} Propchange: commons/proper/dbutils/trunk/src/test/org/apache/commons/dbutils/AsyncQueryRunnerTest.java ------------------------------------------------------------------------------ svn:eol-style = native