diff --git a/jdbc-driver/src/main/java/acolyte/jdbc/Connection.java b/jdbc-driver/src/main/java/acolyte/jdbc/Connection.java index 43d3ceda..78ebe460 100644 --- a/jdbc-driver/src/main/java/acolyte/jdbc/Connection.java +++ b/jdbc-driver/src/main/java/acolyte/jdbc/Connection.java @@ -204,6 +204,10 @@ public void commit() throws SQLException { if (this.autoCommit) { throw new SQLException("Auto-commit is enabled"); } // end of if + + // --- + + handler.getResourceHandler().whenCommitTransaction(this); } // end of /** @@ -215,6 +219,10 @@ public void rollback() throws SQLException { if (this.autoCommit) { throw new SQLException("Auto-commit is enabled"); } // end of if + + // --- + + handler.getResourceHandler().whenRollbackTransaction(this); } // end of rollback /** diff --git a/jdbc-driver/src/main/java/acolyte/jdbc/ConnectionHandler.java b/jdbc-driver/src/main/java/acolyte/jdbc/ConnectionHandler.java index 5c07589e..0d6ad926 100644 --- a/jdbc-driver/src/main/java/acolyte/jdbc/ConnectionHandler.java +++ b/jdbc-driver/src/main/java/acolyte/jdbc/ConnectionHandler.java @@ -14,24 +14,56 @@ public interface ConnectionHandler { */ public StatementHandler getStatementHandler(); - // --- Inner classes --- + /** + * Returns resource handler. + * @return the resource handler + */ + public ResourceHandler getResourceHandler(); + + /** + * Returns this connection handler with its resource |handler| updated. + * + * @param handler the new resource handler + */ + public ConnectionHandler withResourceHandler(ResourceHandler handler); + + // --- Inner classes --- /** * Default implementation. */ public static final class Default implements ConnectionHandler { final StatementHandler stmtHandler; + final ResourceHandler resHandler; + + /** + * Statement constructor. + * + * @param stmtHandler the statement handler + */ + public Default(final StatementHandler stmtHandler) { + this(stmtHandler, new ResourceHandler.Default()); + } /** * Bulk constructor. - * @param handler the statement handler + * + * @param stmtHandler the statement handler + * @param resHandler the resource handler */ - public Default(final StatementHandler handler) { - if (handler == null) { - throw new IllegalArgumentException(); + public Default(final StatementHandler stmtHandler, + final ResourceHandler resHandler) { + + if (stmtHandler == null) { + throw new IllegalArgumentException("Statement handler"); } // end of if - this.stmtHandler = handler; + if (resHandler == null) { + throw new IllegalArgumentException("Resource handler"); + } // end of if + + this.stmtHandler = stmtHandler; + this.resHandler = resHandler; } // end of /** @@ -40,5 +72,19 @@ public Default(final StatementHandler handler) { public StatementHandler getStatementHandler() { return this.stmtHandler; } // end of getStatementHandler + + /** + * {@inheritDoc} + */ + public ResourceHandler getResourceHandler() { + return this.resHandler; + } // end of getResourceHandler + + /** + * {@inheritDoc} + */ + public ConnectionHandler withResourceHandler(ResourceHandler handler) { + return new Default(this.stmtHandler, handler); + } // end of withResourceHandler } // end of class DefaultHandler } // end of interface ConnectionHandler diff --git a/jdbc-driver/src/main/java/acolyte/jdbc/Driver.java b/jdbc-driver/src/main/java/acolyte/jdbc/Driver.java index 24b7fa8e..a946efaa 100644 --- a/jdbc-driver/src/main/java/acolyte/jdbc/Driver.java +++ b/jdbc-driver/src/main/java/acolyte/jdbc/Driver.java @@ -180,7 +180,6 @@ public static acolyte.jdbc.Connection connection(final ConnectionHandler handler * @see #connection(acolyte.jdbc.ConnectionHandler) */ public static acolyte.jdbc.Connection connection(final ConnectionHandler handler, final Properties info) { - if (handler == null) { throw new IllegalArgumentException(); } // end of if @@ -211,12 +210,31 @@ public static acolyte.jdbc.Connection connection(StatementHandler handler) { * @throws IllegalArgumentException if handler is null */ public static acolyte.jdbc.Connection connection(final StatementHandler handler, final Properties info) { + return connection(handler, new ResourceHandler.Default(), info); + } // end of connection - if (handler == null) { - throw new IllegalArgumentException(); + /** + * Direct connection, with given |handler| and random URL. + * + * @param stmtHandler the statement handler + * @param resHandler the resource handler + * @param info Connection properties (optional) + * @return the configured connection + * @throws IllegalArgumentException if handler is null + */ + public static acolyte.jdbc.Connection connection(final StatementHandler stmtHandler, final ResourceHandler resHandler, final Properties info) { + + if (stmtHandler == null) { + throw new IllegalArgumentException("Statement handler"); } // end of if - return connection(new ConnectionHandler.Default(handler), info); + if (resHandler == null) { + throw new IllegalArgumentException("Resource handler"); + } // end of if + + return connection(new ConnectionHandler. + Default(stmtHandler, resHandler), info); + } // end of connection /** diff --git a/jdbc-driver/src/main/java/acolyte/jdbc/ResourceHandler.java b/jdbc-driver/src/main/java/acolyte/jdbc/ResourceHandler.java new file mode 100644 index 00000000..8b15652c --- /dev/null +++ b/jdbc-driver/src/main/java/acolyte/jdbc/ResourceHandler.java @@ -0,0 +1,48 @@ +package acolyte.jdbc; + +import java.sql.SQLException; + +/** + * Resource handler: allow to intercept management operations + * about the connection resources. + */ +public interface ResourceHandler { + /** + * Is fired when the transaction of |connection| is commited + * (but not for implicit commit in case of auto-commit). + * + * @see java.sql.Connection#commit + */ + public void whenCommitTransaction(Connection connection) + throws SQLException; + + /** + * Is fired when the transaction of |connection| is rollbacked. + * + * @see java.sql.Connection#rollback + */ + public void whenRollbackTransaction(Connection connection) + throws SQLException; + + // --- Inner classes --- + + /** + * Default implementation. + */ + public static final class Default implements ResourceHandler { + public Default() { + } + + /** + * {@inheritDoc} + */ + public void whenCommitTransaction(Connection connection) + throws SQLException {} + + /** + * {@inheritDoc} + */ + public void whenRollbackTransaction(Connection connection) + throws SQLException {} + } +} diff --git a/jdbc-driver/src/test/scala/acolyte/jdbc/ConnectionHandlerSpec.scala b/jdbc-driver/src/test/scala/acolyte/jdbc/ConnectionHandlerSpec.scala index 7698e5bc..5da19004 100644 --- a/jdbc-driver/src/test/scala/acolyte/jdbc/ConnectionHandlerSpec.scala +++ b/jdbc-driver/src/test/scala/acolyte/jdbc/ConnectionHandlerSpec.scala @@ -8,8 +8,27 @@ object ConnectionHandlerSpec extends Specification { "Default handler" should { "refuse null statement handler" in { new ConnectionHandler.Default(null). + aka("ctor") must throwA[IllegalArgumentException]("Statement handler") + + } + + "refuse null resource handler" in { + new ConnectionHandler.Default(null, null). aka("ctor") must throwA[IllegalArgumentException] } + + "update the resource handler" in { + val conHandler1 = new ConnectionHandler.Default( + test.EmptyStatementHandler) + + val resHandler2 = new ResourceHandler.Default() + val conHandler2 = conHandler1.withResourceHandler(resHandler2) + + conHandler1.hashCode must not(beEqualTo(conHandler2.hashCode)) and { + conHandler2.getResourceHandler.hashCode must not(beEqualTo( + conHandler1.getResourceHandler.hashCode)) + } + } } } diff --git a/jdbc-driver/src/test/scala/acolyte/jdbc/ConnectionSpec.scala b/jdbc-driver/src/test/scala/acolyte/jdbc/ConnectionSpec.scala index a241be7d..3af2706e 100644 --- a/jdbc-driver/src/test/scala/acolyte/jdbc/ConnectionSpec.scala +++ b/jdbc-driver/src/test/scala/acolyte/jdbc/ConnectionSpec.scala @@ -287,6 +287,51 @@ object ConnectionSpec extends Specification with ConnectionFixtures { } + "be intercepted" >> { + "successfully" in { + @volatile var rollback = 0 + + val conHandler = new ConnectionHandler { + def getStatementHandler = test.EmptyStatementHandler + + def getResourceHandler = new ResourceHandler { + def whenCommitTransaction(c: Connection): Unit = () + + def whenRollbackTransaction(c: Connection): Unit = { + rollback += 1 + } + } + + def withResourceHandler(h: ResourceHandler): ConnectionHandler = + new ConnectionHandler.Default(test.EmptyStatementHandler, h) + } + + connection(jdbcUrl, emptyClientInfo, conHandler). + rollback() must not(throwA[SQLException]) and { + rollback must_=== 1 + } + } + + "with exception" in { + val conHandler = new ConnectionHandler { + def getStatementHandler = test.EmptyStatementHandler + + def getResourceHandler = new ResourceHandler { + def whenCommitTransaction(c: Connection): Unit = () + + def whenRollbackTransaction(c: Connection): Unit = + throw new SQLException("Foo") + } + + def withResourceHandler(h: ResourceHandler): ConnectionHandler = + new ConnectionHandler.Default(test.EmptyStatementHandler, h) + } + + connection(jdbcUrl, emptyClientInfo, conHandler). + rollback() must throwA[SQLException]("Foo") + } + } + "not be applied on closed connection" in { lazy val c = defaultCon c.close() @@ -409,6 +454,51 @@ object ConnectionSpec extends Specification with ConnectionFixtures { message = "Auto-commit is enabled")) } + + "be intercepted" >> { + "successfully" in { + @volatile var commit = 0 + + val conHandler = new ConnectionHandler { + def getStatementHandler = test.EmptyStatementHandler + + def getResourceHandler = new ResourceHandler { + def whenCommitTransaction(c: Connection): Unit = { + commit += 1 + } + + def whenRollbackTransaction(c: Connection): Unit = () + } + + def withResourceHandler(h: ResourceHandler): ConnectionHandler = + new ConnectionHandler.Default(test.EmptyStatementHandler, h) + } + + connection(jdbcUrl, emptyClientInfo, conHandler). + commit() must not(throwA[SQLException]) and { + commit must_=== 1 + } + } + + "with exception" in { + val conHandler = new ConnectionHandler { + def getStatementHandler = test.EmptyStatementHandler + + def getResourceHandler = new ResourceHandler { + def whenCommitTransaction(c: Connection): Unit = + throw new SQLException("Bar") + + def whenRollbackTransaction(c: Connection): Unit = () + } + + def withResourceHandler(h: ResourceHandler): ConnectionHandler = + new ConnectionHandler.Default(test.EmptyStatementHandler, h) + } + + connection(jdbcUrl, emptyClientInfo, conHandler). + commit() must throwA[SQLException]("Bar") + } + } } "Auto-commit mode" should { @@ -808,5 +898,5 @@ sealed trait ConnectionFixtures { def defaultCon = connection(jdbcUrl, null, defaultHandler) - def connection(url: String, props: java.util.Properties, handler: ConnectionHandler) = new acolyte.jdbc.Connection(url, props, handler) + def connection(url: String, props: java.util.Properties, handler: ConnectionHandler) = new Connection(url, props, handler) } diff --git a/jdbc-driver/src/test/scala/acolyte/jdbc/test.scala b/jdbc-driver/src/test/scala/acolyte/jdbc/test.scala index 730d938a..8a9e9349 100644 --- a/jdbc-driver/src/test/scala/acolyte/jdbc/test.scala +++ b/jdbc-driver/src/test/scala/acolyte/jdbc/test.scala @@ -9,6 +9,10 @@ package object test { object EmptyConnectionHandler extends ConnectionHandler { def getStatementHandler = EmptyStatementHandler + def getResourceHandler = new ResourceHandler.Default() + + def withResourceHandler(h: ResourceHandler): ConnectionHandler = + new ConnectionHandler.Default(EmptyStatementHandler, h) } object EmptyStatementHandler extends StatementHandler { diff --git a/jdbc-scala/src/main/scala/AcolyteDSL.scala b/jdbc-scala/src/main/scala/AcolyteDSL.scala index dcbb29cd..359edc22 100644 --- a/jdbc-scala/src/main/scala/AcolyteDSL.scala +++ b/jdbc-scala/src/main/scala/AcolyteDSL.scala @@ -8,8 +8,8 @@ import java.sql.{ Connection ⇒ SqlConnection, SQLException } import scala.language.implicitConversions import scala.collection.JavaConverters._ -import acolyte.jdbc.StatementHandler.Parameter import acolyte.jdbc.AbstractCompositeHandler.{ QueryHandler, UpdateHandler } +import acolyte.jdbc.StatementHandler.Parameter import acolyte.jdbc.RowList.{ Column ⇒ Col } /** @@ -31,8 +31,8 @@ object AcolyteDSL { /** * Creates a connection, whose statement will be passed to given handler. * - * @param h statement handler - * @param p connection properties + * @param sh the statement handler + * @param p the connection properties * @return a new Acolyte connection * * {{{ @@ -42,8 +42,30 @@ object AcolyteDSL { * connection(handler, "acolyte.parameter.untypedNull" -> "true") * }}} */ - def connection(h: AbstractCompositeHandler[_], p: (String, String)*) = - Driver.connection(h, p.foldLeft(new java.util.Properties()) { (ps, t) ⇒ + def connection(sh: AbstractCompositeHandler[_], p: (String, String)*): Connection = Driver.connection(sh, p.foldLeft(new java.util.Properties()) { (ps, t) ⇒ + ps.put(t._1, t._2); ps + }) + + /** + * Creates a connection, whose statement will be passed to given handler. + * + * @param sh the statement handler + * @param rh the resource handler + * @param p the connection properties + * @return a new Acolyte connection + * + * {{{ + * connection(handler) // without connection properties + * + * // With connection property to fallback untyped null + * connection(handler, "acolyte.parameter.untypedNull" -> "true") + * }}} + */ + def connection( + sh: AbstractCompositeHandler[_], + rh: ResourceHandler, + p: (String, String)*): Connection = Driver.connection(sh, rh, + p.foldLeft(new java.util.Properties()) { (ps, t) ⇒ ps.put(t._1, t._2); ps }) @@ -75,7 +97,8 @@ object AcolyteDSL { * connection { handleStatement } * }}} */ - def handleStatement = ScalaCompositeHandler.empty + @inline def handleStatement: ScalaCompositeHandler = + ScalaCompositeHandler.empty /** * Creates a new handler detecting all statements as queries @@ -118,6 +141,23 @@ object AcolyteDSL { def updateResult(count: Int, keys: RowList[_]): UpdateResult = new UpdateResult(count) withGeneratedKeys keys + /** + * Returns a resource handler intercepting transaction commit or rollback. + * + * @param whenCommit the function handling commit + * @param whenRollback the function handling rollback + * + * @see [[java.sql.Connection.commit]] + * @see [[java.sql.Connection.rollback]] + */ + def handleTransaction( + whenCommit: Connection ⇒ Unit = { _ ⇒ () }, + whenRollback: Connection ⇒ Unit = { _ ⇒ () }): ResourceHandler = + new ResourceHandler { + def whenCommitTransaction(con: Connection) = whenCommit(con) + def whenRollbackTransaction(con: Connection) = whenRollback(con) + } + /** * Manages a scope to debug any JDBC execution * diff --git a/jdbc-scala/src/test/scala/acolyte/jdbc/ConnectionSpec.scala b/jdbc-scala/src/test/scala/acolyte/jdbc/ConnectionSpec.scala index b7cd1a15..e91145e4 100644 --- a/jdbc-scala/src/test/scala/acolyte/jdbc/ConnectionSpec.scala +++ b/jdbc-scala/src/test/scala/acolyte/jdbc/ConnectionSpec.scala @@ -17,6 +17,31 @@ object ConnectionSpec extends org.specs2.mutable.Specification { } } + "Transaction" should { + "handle commit" in { + @volatile var commit = 0 + val con = AcolyteDSL.connection( + EmptyHandler, + AcolyteDSL.handleTransaction(whenCommit = { _ ⇒ commit += 1 })) + + con.commit() must not(throwA[Exception]) and { + commit must_== 1 + } + } + + "handle rollback" in { + val rollbacked = scala.concurrent.Promise[Connection]() + + val con = AcolyteDSL.connection( + EmptyHandler, + AcolyteDSL.handleTransaction(whenRollback = rollbacked.success)) + + con.rollback() must not(throwA[Exception]) and { + rollbacked.isCompleted must beTrue + } + } + } + "Debug" should { "be successful" in { val output = Seq.newBuilder[String] diff --git a/jdbc-scala/src/test/scala/acolyte/jdbc/ExecutionSpec.scala b/jdbc-scala/src/test/scala/acolyte/jdbc/ExecutionSpec.scala index dce2e619..9b4531b0 100644 --- a/jdbc-scala/src/test/scala/acolyte/jdbc/ExecutionSpec.scala +++ b/jdbc-scala/src/test/scala/acolyte/jdbc/ExecutionSpec.scala @@ -1,5 +1,7 @@ package acolyte.jdbc +import java.sql.SQLException + import acolyte.jdbc.{ ExecutedParameter ⇒ XP } object ExecutionSpec extends org.specs2.mutable.Specification { @@ -90,4 +92,20 @@ object ExecutionSpec extends org.specs2.mutable.Specification { } } } + + "Update execution" should { + "represent DB error" in { + val handler = AcolyteDSL.handleStatement.withUpdateHandler { _ ⇒ + throw new SQLException("Foo bar lorem") + } + + val con = AcolyteDSL.connection(handler) + val stmt = con.prepareStatement("UPDATE tbl SET nme = ? WHERE id = ?") + + stmt.setString(1, "test") + stmt.setString(2, "this") + + stmt.executeUpdate() must throwA[SQLException]("Foo bar lorem") + } + } } diff --git a/version.sbt b/version.sbt index 1bef01f5..d8e70901 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "1.0.49" +version in ThisBuild := "1.0.50"