excludePaths = new HashSet<>();
+ environment.jersey().register(new JdbiUnitOfWorkApplicationEventListener(handleManager, excludePaths));
+ ```
+
+
+
+- Start annotating resource methods with `@JdbiUnitOfWork` and you're good to go.
+ ```java
+ @POST
+ @Path("/")
+ @JdbiUnitOfWork
+ public RequestResponse createRequest() {
+ ..do stateful work (across multiple Dao's)
+ return response
+ }
+ ```
diff --git a/src/main/java/io/dropwizard/jdbi/unitofwork/core/DefaultJdbiHandleManager.java b/src/main/java/io/dropwizard/jdbi/unitofwork/core/DefaultJdbiHandleManager.java
new file mode 100644
index 0000000..4299b2e
--- /dev/null
+++ b/src/main/java/io/dropwizard/jdbi/unitofwork/core/DefaultJdbiHandleManager.java
@@ -0,0 +1,39 @@
+package io.dropwizard.jdbi.unitofwork.core;
+
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.Handle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This implementation gets a new handle each time it is invoked. It simulates the default
+ * behaviour of creating new handles each time the dao method is invoked.
+ *
+ * It can be used to service requests which interact with only a single method in a single handle.
+ * This is a lightweight implementation suitable for testing, such as with embedded databases.
+ * Any serious application should not be using this as it may quickly leak / run out of handles
+ *
+ * @apiNote Not suitable for requests spanning multiple Dbi as the handle returned is different
+ * This implementation, therefore, does not support thread factory creation.
+ */
+public class DefaultJdbiHandleManager implements JdbiHandleManager {
+
+ private final Logger log = LoggerFactory.getLogger(DefaultJdbiHandleManager.class);
+ private final DBI dbi;
+
+ public DefaultJdbiHandleManager(DBI dbi) {
+ this.dbi = dbi;
+ }
+
+ @Override
+ public Handle get() {
+ Handle handle = dbi.open();
+ log.debug("handle [{}] : Thread Id [{}]", handle.hashCode(), Thread.currentThread().getId());
+ return handle;
+ }
+
+ @Override
+ public void clear() {
+ log.debug("No Op");
+ }
+}
diff --git a/src/main/java/io/dropwizard/jdbi/unitofwork/core/JdbiHandleManager.java b/src/main/java/io/dropwizard/jdbi/unitofwork/core/JdbiHandleManager.java
new file mode 100644
index 0000000..ec39fc0
--- /dev/null
+++ b/src/main/java/io/dropwizard/jdbi/unitofwork/core/JdbiHandleManager.java
@@ -0,0 +1,55 @@
+package io.dropwizard.jdbi.unitofwork.core;
+
+import org.skife.jdbi.v2.Handle;
+
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * A {@link JdbiHandleManager} is used to provide the lifecycle of a {@link Handle} with respect
+ * to a given scope. A scope may be session based, request based or may be invoked on every run.
+ */
+public interface JdbiHandleManager {
+
+ /**
+ * Provide a way to get a Jdbi handle, a wrapped connection to the underlying database
+ *
+ * @return a valid handle tied with a specific scope
+ */
+ Handle get();
+
+ /**
+ * Provide a way to clear the handle rendering it useless for the other methods
+ */
+ void clear();
+
+ /**
+ * Provide a thread factory for the caller with some identity represented by the
+ * {@link #getConversationId()}. This can be used by the caller to create multiple threads,
+ * say, using {@link java.util.concurrent.ExecutorService}. The {@link JdbiHandleManager} can
+ * then use the thread factory to identify and manage handle use across multiple threads.
+ *
+ * @return a thread factory used to safely create multiple threads
+ * @throws UnsupportedOperationException by default. Implementations overriding this method
+ * must ensure that the conversation id is unique
+ */
+ default ThreadFactory createThreadFactory() {
+ throw new UnsupportedOperationException("Thread factory creation is not supported");
+ }
+
+ /**
+ * Provide a unique identifier for the conversation with a handle. No two identifiers
+ * should co exist at once during the application lifecycle or else handle corruption
+ * or misuse might occur.
+ *
+ * This can be relied upon by the {@link #createThreadFactory()} to reuse handles across
+ * multiple threads spawned off a request thread.
+ *
+ * @return a unique identifier applicable to a scope
+ * @implNote hashcode can not be relied upon for providing a unique identifier due to the
+ * possibility of collision. Instead opt for a monotonically increasing counter, such as
+ * the thread id.
+ */
+ default String getConversationId() {
+ return String.valueOf(Thread.currentThread().getId());
+ }
+}
diff --git a/src/main/java/io/dropwizard/jdbi/unitofwork/core/JdbiUnitOfWorkProvider.java b/src/main/java/io/dropwizard/jdbi/unitofwork/core/JdbiUnitOfWorkProvider.java
new file mode 100644
index 0000000..13e077c
--- /dev/null
+++ b/src/main/java/io/dropwizard/jdbi/unitofwork/core/JdbiUnitOfWorkProvider.java
@@ -0,0 +1,129 @@
+package io.dropwizard.jdbi.unitofwork.core;
+
+import com.google.common.collect.Sets;
+import com.google.common.reflect.Reflection;
+import org.reflections.Reflections;
+import org.reflections.scanners.Scanners;
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.sqlobject.SqlBatch;
+import org.skife.jdbi.v2.sqlobject.SqlCall;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@SuppressWarnings({"UnstableApiUsage", "rawtypes", "unchecked"})
+public class JdbiUnitOfWorkProvider {
+
+ private final Logger log = LoggerFactory.getLogger(JdbiUnitOfWorkProvider.class);
+ private final JdbiHandleManager handleManager;
+
+ private JdbiUnitOfWorkProvider(JdbiHandleManager handleManager) {
+ this.handleManager = handleManager;
+ }
+
+ public static JdbiUnitOfWorkProvider withDefault(DBI dbi) {
+ JdbiHandleManager handleManager = new RequestScopedJdbiHandleManager(dbi);
+ return new JdbiUnitOfWorkProvider(handleManager);
+ }
+
+ public static JdbiUnitOfWorkProvider withLinked(DBI dbi) {
+ JdbiHandleManager handleManager = new LinkedRequestScopedJdbiHandleManager(dbi);
+ return new JdbiUnitOfWorkProvider(handleManager);
+ }
+
+ public JdbiHandleManager getHandleManager() {
+ return handleManager;
+ }
+
+ /**
+ * getWrappedInstanceForDaoClass generates a proxy instance of the dao class for which
+ * the jdbi unit of work aspect would be wrapped around with.
+ *
+ * Note: It is recommended to use {@link JdbiUnitOfWorkProvider#getWrappedInstanceForDaoPackage(List)} instead
+ * as passing a list of packages is easier than passing each instance individually.
+ *
+ * This method however may be used in case the classpath scanning is disabled.
+ * If the original class is null or contains no relevant JDBI annotations, this method throws an
+ * exception
+ *
+ * @param daoClass the DAO class for which a proxy needs to be created fo
+ * @return the wrapped instance ready to be passed around
+ */
+ public Object getWrappedInstanceForDaoClass(Class daoClass) {
+ if (daoClass == null) {
+ throw new IllegalArgumentException("DAO Class cannot be null");
+ }
+ boolean atLeastOneJdbiMethod = false;
+ for (Method method : daoClass.getDeclaredMethods()) {
+ if (method.getDeclaringClass() == daoClass) {
+ atLeastOneJdbiMethod = method.getAnnotation(SqlQuery.class) != null;
+ atLeastOneJdbiMethod = atLeastOneJdbiMethod || method.getAnnotation(SqlUpdate.class) != null;
+ atLeastOneJdbiMethod = atLeastOneJdbiMethod || method.getAnnotation(SqlUpdate.class) != null;
+ atLeastOneJdbiMethod = atLeastOneJdbiMethod || method.getAnnotation(SqlBatch.class) != null;
+ atLeastOneJdbiMethod = atLeastOneJdbiMethod || method.getAnnotation(SqlCall.class) != null;
+ }
+ }
+ if (!atLeastOneJdbiMethod) {
+ throw new IllegalArgumentException(String.format("Class [%s] has no method annotated with a Jdbi SQL Object", daoClass.getSimpleName()));
+ }
+
+ log.info("Binding class [{}] with proxy handler [{}] ", daoClass.getSimpleName(), handleManager.getClass().getSimpleName());
+ ManagedHandleInvocationHandler handler = new ManagedHandleInvocationHandler<>(handleManager, daoClass);
+ Object proxiedInstance = Reflection.newProxy(daoClass, handler);
+ return daoClass.cast(proxiedInstance);
+ }
+
+ /**
+ * getWrappedInstanceForDaoPackage generates a map where every DAO class identified
+ * through the given list of packages is mapped to its initialised proxy instance
+ * the jdbi unit of work aspect would be wrapped around with.
+ *
+ * In case classpath scanning is disabled, use {@link JdbiUnitOfWorkProvider#getWrappedInstanceForDaoClass(Class)}
+ *
+ * If the original package list is null, this method throws an exception
+ *
+ * @param daoPackages the list of packages that contain the DAO classes
+ * @return the map mapping dao classes to its initialised proxies
+ */
+ public Map extends Class, Object> getWrappedInstanceForDaoPackage(List daoPackages) {
+ if (daoPackages == null) {
+ throw new IllegalArgumentException("DAO Class package list cannot be null");
+ }
+
+ Set extends Class>> allDaoClasses = daoPackages.stream()
+ .map(this::getDaoClassesForPackage)
+ .flatMap(Collection::stream)
+ .collect(Collectors.toSet());
+
+ Map classInstanceMap = new HashMap<>();
+ for (Class klass : allDaoClasses) {
+ log.info("Binding class [{}] with proxy handler [{}] ", klass.getSimpleName(), handleManager.getClass().getSimpleName());
+ Object instance = getWrappedInstanceForDaoClass(klass);
+ classInstanceMap.put(klass, instance);
+ }
+ return classInstanceMap;
+ }
+
+ private Set extends Class>> getDaoClassesForPackage(String pkg) {
+ Set daoClasses = new HashSet<>();
+
+ Sets.SetView union = Sets.union(daoClasses, new Reflections(pkg, Scanners.MethodsAnnotated).getMethodsAnnotatedWith(SqlQuery.class));
+ union = Sets.union(union, new Reflections(pkg, Scanners.MethodsAnnotated).getMethodsAnnotatedWith(SqlUpdate.class));
+ union = Sets.union(union, new Reflections(pkg, Scanners.MethodsAnnotated).getMethodsAnnotatedWith(SqlBatch.class));
+ union = Sets.union(union, new Reflections(pkg, Scanners.MethodsAnnotated).getMethodsAnnotatedWith(SqlCall.class));
+
+ return union.stream()
+ .map(Method::getDeclaringClass)
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/src/main/java/io/dropwizard/jdbi/unitofwork/core/LinkedRequestScopedJdbiHandleManager.java b/src/main/java/io/dropwizard/jdbi/unitofwork/core/LinkedRequestScopedJdbiHandleManager.java
new file mode 100644
index 0000000..53371d1
--- /dev/null
+++ b/src/main/java/io/dropwizard/jdbi/unitofwork/core/LinkedRequestScopedJdbiHandleManager.java
@@ -0,0 +1,99 @@
+package io.dropwizard.jdbi.unitofwork.core;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.Handle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nullable;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * This implementation provides a handle scoped to a thread and all other threads Y spawned from X
+ * All Y threads must follow a particular name format extracted from the conversation id
+ * This is one of the ways the manager can know of the grouping and establish re-usability of
+ * handles across such grouped threads.
+ *
+ * It can be used to service requests where only a single handle instance has to be used by multiple
+ * threads that are spawned with the specified name format from an initial thread. Use this only
+ * when you have complete control over the threads you create. The threads must not run once the
+ * parent thread is returned to the pool or else the handles will be invalid or in other words
+ * parent thread must block on the results of children.
+ *
+ *
+ * It relies on the fact that the {@code Jdbi.Handle} is inherently thread safe and can be used to service
+ * dao requests between multiple threads.
+ * Note: Not suitable when you can not set the name format for the newly spawned threads.
+ **/
+class LinkedRequestScopedJdbiHandleManager implements JdbiHandleManager {
+
+ private final Logger log = LoggerFactory.getLogger(LinkedRequestScopedJdbiHandleManager.class);
+ private final Map parentThreadHandleMap = new ConcurrentHashMap<>();
+ private final DBI dbi;
+
+ public LinkedRequestScopedJdbiHandleManager(DBI dbi) {
+ this.dbi = dbi;
+ }
+
+ @Override
+ public Handle get() {
+ String parent = substringBetween(Thread.currentThread().getName());
+ Handle handle;
+ if (parent == null) {
+ handle = getHandle();
+ log.debug("Owner of handle [{}] : Parent Thread Id [{}]", handle.hashCode(), Thread.currentThread().getId());
+
+ } else {
+ handle = parentThreadHandleMap.get(parent);
+ if (handle == null) {
+ throw new IllegalStateException(String.format("Handle to be reused in child thread [%s] is null for parent thread [%s]", Thread.currentThread().getId(), parent));
+ }
+ log.debug("Reusing parent thread handle [{}] for [{}]", handle.hashCode(), Thread.currentThread().getId());
+ }
+ return handle;
+ }
+
+ @Override
+ public void clear() {
+ String parent = getConversationId();
+ Handle handle = parentThreadHandleMap.get(parent);
+ if (handle != null) {
+ handle.close();
+ log.debug("Closed handle Thread Id [{}] has handle id [{}]", Thread.currentThread().getId(), handle.hashCode());
+
+ parentThreadHandleMap.remove(parent);
+ log.debug("Clearing handle member for parent thread [{}] ", Thread.currentThread().getId());
+ }
+ }
+
+ @Override
+ public ThreadFactory createThreadFactory() {
+ String threadName = String.format("[%s]-%%d", getConversationId());
+ return new ThreadFactoryBuilder().setNameFormat(threadName).build();
+ }
+
+ private Handle getHandle() {
+ String threadIdentity = getConversationId();
+ if (parentThreadHandleMap.containsKey(threadIdentity)) {
+ return parentThreadHandleMap.get(threadIdentity);
+ }
+ Handle handle = dbi.open();
+ parentThreadHandleMap.putIfAbsent(threadIdentity, handle);
+ return handle;
+ }
+
+ @Nullable
+ private String substringBetween(String threadName) {
+ final int start = threadName.indexOf("[");
+ if (start != -1) {
+ final int end = threadName.indexOf("]", start + "[".length());
+ if (end != -1) {
+ return threadName.substring(start + "[".length(), end);
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/io/dropwizard/jdbi/unitofwork/core/ManagedHandleInvocationHandler.java b/src/main/java/io/dropwizard/jdbi/unitofwork/core/ManagedHandleInvocationHandler.java
new file mode 100644
index 0000000..85d2a84
--- /dev/null
+++ b/src/main/java/io/dropwizard/jdbi/unitofwork/core/ManagedHandleInvocationHandler.java
@@ -0,0 +1,63 @@
+package io.dropwizard.jdbi.unitofwork.core;
+
+import org.skife.jdbi.v2.Handle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Implementation of {@link InvocationHandler} that attaches the underlying class to a handle
+ * obtained through {@link JdbiHandleManager} on every invocation.
+ *
+ * Note: Attaching a handle to a class is an idempotent operation. If a handle {@literal H}
+ * is attached to a class, attaching {@literal H} to the same class again serves no purpose.
+ *
+ * Also delegates {@link Object#toString} to the real object instead of the proxy which is
+ * helpful for debugging
+ */
+public class ManagedHandleInvocationHandler implements InvocationHandler {
+
+ private final Logger log = LoggerFactory.getLogger(ManagedHandleInvocationHandler.class);
+ private static final Object[] NO_ARGS = {};
+ private final JdbiHandleManager handleManager;
+ private final Class underlying;
+
+ public ManagedHandleInvocationHandler(JdbiHandleManager handleManager, Class underlying) {
+ this.handleManager = handleManager;
+ this.underlying = underlying;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * - {@code proxy.toString()} delegates to {@link ManagedHandleInvocationHandler#toString}
+ *
- other method calls are dispatched to {@link ManagedHandleInvocationHandler#handleInvocation}.
+ *
+ */
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ if (args == null) {
+ args = NO_ARGS;
+ }
+ if (args.length == 0 && method.getName().equals("toString")) {
+ return toString();
+ }
+ return handleInvocation(method, args);
+ }
+
+ private Object handleInvocation(Method method, Object[] args) throws IllegalAccessException, InvocationTargetException {
+ Handle handle = handleManager.get();
+ log.debug("{}.{} [{}] Thread Id [{}] with handle id [{}]", method.getDeclaringClass().getSimpleName(), method.getName(), underlying.getSimpleName(), Thread.currentThread().getId(), handle.hashCode());
+
+ Object dao = handle.attach(underlying);
+ return method.invoke(dao, args);
+ }
+
+ @Override
+ public String toString() {
+ return "Proxy[" + underlying.getSimpleName() + "]";
+ }
+}
diff --git a/src/main/java/io/dropwizard/jdbi/unitofwork/core/RequestScopedJdbiHandleManager.java b/src/main/java/io/dropwizard/jdbi/unitofwork/core/RequestScopedJdbiHandleManager.java
new file mode 100644
index 0000000..cf51580
--- /dev/null
+++ b/src/main/java/io/dropwizard/jdbi/unitofwork/core/RequestScopedJdbiHandleManager.java
@@ -0,0 +1,50 @@
+package io.dropwizard.jdbi.unitofwork.core;
+
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.Handle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This implementation gets a new handle which is scoped to the thread requesting the handle.
+ *
+ * It can be used to service requests which interact with multiple SQL objects as part of a common
+ * transaction. All such SQL objects will be attached to the common handle.
+ *
+ * @apiNote Not suitable for requests which spawn new threads from the requesting thread as the scoped
+ * handle is not preserved. This implementation, therefore, does not support thread factory creation
+ */
+class RequestScopedJdbiHandleManager implements JdbiHandleManager {
+
+ private final Logger log = LoggerFactory.getLogger(RequestScopedJdbiHandleManager.class);
+ private final DBI dbi;
+
+ @SuppressWarnings("ThreadLocalUsage")
+ private final ThreadLocal threadLocal = new ThreadLocal<>();
+
+ public RequestScopedJdbiHandleManager(DBI dbi) {
+ this.dbi = dbi;
+ }
+
+ @Override
+ public Handle get() {
+ if (threadLocal.get() == null) {
+ threadLocal.set(dbi.open());
+ }
+ Handle handle = threadLocal.get();
+ log.debug("handle [{}] : Thread Id [{}]", handle.hashCode(), Thread.currentThread().getId());
+ return handle;
+ }
+
+ @Override
+ public void clear() {
+ Handle handle = threadLocal.get();
+ if (handle != null) {
+ handle.close();
+ log.debug("Closed handle Thread Id [{}] has handle id [{}]", Thread.currentThread().getId(), handle.hashCode());
+
+ threadLocal.remove();
+ log.debug("Clearing handle member for thread [{}] ", Thread.currentThread().getId());
+ }
+ }
+}
diff --git a/src/main/java/io/dropwizard/jdbi/unitofwork/listener/HttpGetRequestJdbiUnitOfWorkEventListener.java b/src/main/java/io/dropwizard/jdbi/unitofwork/listener/HttpGetRequestJdbiUnitOfWorkEventListener.java
new file mode 100644
index 0000000..2ddc52f
--- /dev/null
+++ b/src/main/java/io/dropwizard/jdbi/unitofwork/listener/HttpGetRequestJdbiUnitOfWorkEventListener.java
@@ -0,0 +1,35 @@
+package io.dropwizard.jdbi.unitofwork.listener;
+
+import io.dropwizard.jdbi.unitofwork.core.JdbiHandleManager;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This listener binds a transaction aspect to the currently serving GET request without creating
+ * any transaction context and is simply responsible for initialising and terminating handles
+ * upon successful start and end of request marked by Jersey request monitoring events
+ * {@code RESOURCE_METHOD_START} and {@code FINISHED} respectively
+ *
+ * For creating a transaction context, see {@link NonHttpGetRequestJdbiUnitOfWorkEventListener}
+ */
+class HttpGetRequestJdbiUnitOfWorkEventListener implements RequestEventListener {
+
+ private final Logger log = LoggerFactory.getLogger(HttpGetRequestJdbiUnitOfWorkEventListener.class);
+ private final JdbiTransactionAspect transactionAspect;
+
+ HttpGetRequestJdbiUnitOfWorkEventListener(JdbiHandleManager handleManager) {
+ this.transactionAspect = new JdbiTransactionAspect(handleManager);
+ }
+
+ @Override
+ public void onEvent(RequestEvent event) {
+ RequestEvent.Type type = event.getType();
+ log.debug("Handling GET Request Event {} {}", type, Thread.currentThread().getId());
+
+ if (type == RequestEvent.Type.FINISHED) {
+ transactionAspect.terminateHandle();
+ }
+ }
+}
diff --git a/src/main/java/io/dropwizard/jdbi/unitofwork/listener/JdbiTransactionAspect.java b/src/main/java/io/dropwizard/jdbi/unitofwork/listener/JdbiTransactionAspect.java
new file mode 100644
index 0000000..bc557bb
--- /dev/null
+++ b/src/main/java/io/dropwizard/jdbi/unitofwork/listener/JdbiTransactionAspect.java
@@ -0,0 +1,69 @@
+package io.dropwizard.jdbi.unitofwork.listener;
+
+import io.dropwizard.jdbi.unitofwork.core.JdbiHandleManager;
+import org.skife.jdbi.v2.Handle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An aspect providing low level operations around a {@link Handle}
+ * This is inspired from Dropwizard's Unit of work aspect used to manage handles for hibernate.
+ *
+ * @see
+ * Unit Of Work Aspect
+ */
+public class JdbiTransactionAspect {
+
+ private final Logger log = LoggerFactory.getLogger(JdbiTransactionAspect.class);
+ private final JdbiHandleManager handleManager;
+
+ public JdbiTransactionAspect(JdbiHandleManager handleManager) {
+ this.handleManager = handleManager;
+ }
+
+ public void begin() {
+ try {
+ Handle handle = handleManager.get();
+ handle.begin();
+ log.debug("Begin Transaction Thread Id [{}] has handle id [{}] Transaction {} Level {}", Thread.currentThread().getId(), handle.hashCode(), handle.isInTransaction(), handle.getTransactionIsolationLevel());
+
+ } catch (Exception ex) {
+ handleManager.clear();
+ throw ex;
+ }
+ }
+
+ public void commit() {
+ Handle handle = handleManager.get();
+ if (handle == null) {
+ log.debug("Handle was found to be null during commit for Thread Id [{}]. It might have already been closed", Thread.currentThread().getId());
+ return;
+ }
+ try {
+ handle.commit();
+ log.debug("Performing commit Thread Id [{}] has handle id [{}] Transaction {} Level {}", Thread.currentThread().getId(), handle.hashCode(), handle.isInTransaction(), handle.getTransactionIsolationLevel());
+
+ } catch (Exception ex) {
+ handle.rollback();
+ throw ex;
+ }
+ }
+
+ public void rollback() {
+ Handle handle = handleManager.get();
+ if (handle == null) {
+ log.debug("Handle was found to be null during rollback for [{}]", Thread.currentThread().getId());
+ return;
+ }
+ try {
+ handle.rollback();
+ log.debug("Performed rollback on Thread Id [{}] has handle id [{}] Transaction {} Level {}", Thread.currentThread().getId(), handle.hashCode(), handle.isInTransaction(), handle.getTransactionIsolationLevel());
+ } finally {
+ terminateHandle();
+ }
+ }
+
+ public void terminateHandle() {
+ handleManager.clear();
+ }
+}
diff --git a/src/main/java/io/dropwizard/jdbi/unitofwork/listener/JdbiUnitOfWorkApplicationEventListener.java b/src/main/java/io/dropwizard/jdbi/unitofwork/listener/JdbiUnitOfWorkApplicationEventListener.java
new file mode 100644
index 0000000..d2737ae
--- /dev/null
+++ b/src/main/java/io/dropwizard/jdbi/unitofwork/listener/JdbiUnitOfWorkApplicationEventListener.java
@@ -0,0 +1,58 @@
+package io.dropwizard.jdbi.unitofwork.listener;
+
+import io.dropwizard.jdbi.unitofwork.core.JdbiUnitOfWorkProvider;
+import org.glassfish.jersey.server.monitoring.ApplicationEvent;
+import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nullable;
+import javax.ws.rs.HttpMethod;
+import java.util.Set;
+
+/**
+ * This application event listener establishes a new request event listener for every request.
+ * The request listener triggers calls appropriate methods on a transaction aspect based on the
+ * request lifecycle methods returned from Jersey
+ *
+ * {@code HttpMethod.GET} requests are assumed to be in non transaction boundary and are routed
+ * to {@link HttpGetRequestJdbiUnitOfWorkEventListener}
+ *
+ * Non {@code HttpMethod.GET} requests are assumed to be in a transaction boundary and are routed
+ * to {@link NonHttpGetRequestJdbiUnitOfWorkEventListener}
+ *
+ * @implNote For requests that never not require a connection with the database, such as ELB health
+ * checks or computate only use cases, opening and closing a handle is redundant and wasteful
+ * Such request URIs should be added in the set of {@link #excludedPaths}
+ */
+public class JdbiUnitOfWorkApplicationEventListener implements ApplicationEventListener {
+
+ private final Logger log = LoggerFactory.getLogger(JdbiUnitOfWorkApplicationEventListener.class);
+ private final JdbiUnitOfWorkProvider unitOfWorkProvider;
+ private final Set excludedPaths;
+
+ public JdbiUnitOfWorkApplicationEventListener(JdbiUnitOfWorkProvider unitOfWorkProvider, Set excludedPaths) {
+ this.unitOfWorkProvider = unitOfWorkProvider;
+ this.excludedPaths = excludedPaths;
+ }
+
+ @Override
+ public void onEvent(ApplicationEvent event) {
+ log.debug("Received Application event {}", event.getType());
+ }
+
+ @Override
+ @Nullable
+ public RequestEventListener onRequest(RequestEvent event) {
+ String path = event.getUriInfo().getPath();
+ if (excludedPaths.stream().anyMatch(path::contains)) {
+ return null;
+ }
+ if (event.getContainerRequest().getMethod().equals(HttpMethod.GET)) {
+ return new HttpGetRequestJdbiUnitOfWorkEventListener(unitOfWorkProvider.getHandleManager());
+ }
+ return new NonHttpGetRequestJdbiUnitOfWorkEventListener(unitOfWorkProvider.getHandleManager());
+ }
+}
diff --git a/src/main/java/io/dropwizard/jdbi/unitofwork/listener/NonHttpGetRequestJdbiUnitOfWorkEventListener.java b/src/main/java/io/dropwizard/jdbi/unitofwork/listener/NonHttpGetRequestJdbiUnitOfWorkEventListener.java
new file mode 100644
index 0000000..14f5431
--- /dev/null
+++ b/src/main/java/io/dropwizard/jdbi/unitofwork/listener/NonHttpGetRequestJdbiUnitOfWorkEventListener.java
@@ -0,0 +1,78 @@
+package io.dropwizard.jdbi.unitofwork.listener;
+
+import io.dropwizard.jdbi.unitofwork.JdbiUnitOfWork;
+import io.dropwizard.jdbi.unitofwork.core.JdbiHandleManager;
+import org.glassfish.jersey.server.model.ResourceMethod;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This listener binds a transaction aspect to the currently serving GET request by creating
+ * a transaction context if and only if the resource method is annotated with {@link JdbiUnitOfWork}
+ *
+ * It is responsible for initialising and terminating handles as well as calling appropriate
+ * transaction methods based on theJersey request monitoring events
+ * {@code RESOURCE_METHOD_START}, {@code RESP_FILTERS_START}, {@code ON_EXCEPTION} and {@code FINISHED}
+ *
+ * For creating a access context without transactions, see {@link HttpGetRequestJdbiUnitOfWorkEventListener}
+ */
+class NonHttpGetRequestJdbiUnitOfWorkEventListener implements RequestEventListener {
+
+ private final Logger log = LoggerFactory.getLogger(NonHttpGetRequestJdbiUnitOfWorkEventListener.class);
+ private final JdbiTransactionAspect transactionAspect;
+
+ NonHttpGetRequestJdbiUnitOfWorkEventListener(JdbiHandleManager handleManager) {
+ this.transactionAspect = new JdbiTransactionAspect(handleManager);
+ }
+
+ @Override
+ public void onEvent(RequestEvent event) {
+ RequestEvent.Type type = event.getType();
+ String httpMethod = event.getContainerRequest().getMethod();
+
+ log.debug("Handling {} Request Event {} {}", httpMethod, type, Thread.currentThread().getId());
+ boolean isTransactional = isTransactional(event);
+
+ if (type == RequestEvent.Type.RESOURCE_METHOD_START) {
+ initialise(isTransactional);
+
+ } else if (type == RequestEvent.Type.RESP_FILTERS_START) {
+ commit(isTransactional);
+
+ } else if (type == RequestEvent.Type.ON_EXCEPTION) {
+ rollback(isTransactional);
+
+ } else if (type == RequestEvent.Type.FINISHED) {
+ transactionAspect.terminateHandle();
+ }
+ }
+
+ private void commit(boolean isTransactional) {
+ if (isTransactional) {
+ transactionAspect.commit();
+ }
+ }
+
+ private void rollback(boolean isTransactional) {
+ if (isTransactional) {
+ transactionAspect.rollback();
+ }
+ }
+
+ private void initialise(boolean isTransactional) {
+ if (isTransactional) {
+ transactionAspect.begin();
+ }
+ }
+
+ private boolean isTransactional(RequestEvent event) {
+ ResourceMethod method = event.getUriInfo().getMatchedResourceMethod();
+ if (method != null) {
+ JdbiUnitOfWork annotation = method.getInvocable().getDefinitionMethod().getAnnotation(JdbiUnitOfWork.class);
+ return annotation != null;
+ }
+ return false;
+ }
+}
diff --git a/src/test/java/io/dropwizard/jdbi/unitofwork/core/DefaultJdbiHandleManagerTest.java b/src/test/java/io/dropwizard/jdbi/unitofwork/core/DefaultJdbiHandleManagerTest.java
new file mode 100644
index 0000000..5f76404
--- /dev/null
+++ b/src/test/java/io/dropwizard/jdbi/unitofwork/core/DefaultJdbiHandleManagerTest.java
@@ -0,0 +1,52 @@
+package io.dropwizard.jdbi.unitofwork.core;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.stubbing.Answer;
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.Handle;
+
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class DefaultJdbiHandleManagerTest {
+
+ private DBI dbi;
+
+ private DefaultJdbiHandleManager manager;
+
+ @BeforeEach
+ public void setUp() {
+ dbi = mock(DBI.class);
+ this.manager = new DefaultJdbiHandleManager(dbi);
+ }
+
+ @Test
+ public void testGetSetsTheHandlePerInvocation() throws InterruptedException {
+ when(dbi.open()).thenAnswer((Answer) invocation -> mock(Handle.class));
+ Handle firstHandle = manager.get();
+ Handle secondHandle = manager.get();
+ assertNotEquals(firstHandle, secondHandle);
+
+ Thread newHandleInvokerThread = new Thread(() -> assertNotEquals(firstHandle, manager.get()));
+ newHandleInvokerThread.start();
+ newHandleInvokerThread.join();
+ verify(dbi, times(3)).open();
+ }
+
+ @Test
+ public void testClear() {
+ manager.clear();
+ verify(dbi, never()).open();
+ }
+
+ @Test
+ public void testCreateThreadFactoryIsNotSupported() {
+ assertThrows(UnsupportedOperationException.class, () -> manager.createThreadFactory());
+ }
+}
diff --git a/src/test/java/io/dropwizard/jdbi/unitofwork/core/JdbiUnitOfWorkProviderTest.java b/src/test/java/io/dropwizard/jdbi/unitofwork/core/JdbiUnitOfWorkProviderTest.java
new file mode 100644
index 0000000..2aeab8c
--- /dev/null
+++ b/src/test/java/io/dropwizard/jdbi/unitofwork/core/JdbiUnitOfWorkProviderTest.java
@@ -0,0 +1,65 @@
+package io.dropwizard.jdbi.unitofwork.core;
+
+import com.google.common.collect.Lists;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class JdbiUnitOfWorkProviderTest {
+
+ @Mock
+ private DBI dbi;
+
+ private JdbiUnitOfWorkProvider provider;
+
+ @BeforeEach
+ public void setUp() {
+ MockitoAnnotations.openMocks(this);
+ this.provider = JdbiUnitOfWorkProvider.withDefault(dbi);
+ }
+
+ @Test
+ public void testGetWrappedInstanceForDaoClass() {
+ assertNotNull(provider.getWrappedInstanceForDaoClass(DaoA.class));
+ assertNotNull(provider.getWrappedInstanceForDaoClass(DaoB.class));
+ assertThrows(IllegalArgumentException.class, () -> provider.getWrappedInstanceForDaoClass(DaoC.class));
+ }
+
+ @Test
+ @SuppressWarnings("rawtypes")
+ public void testGetWrappedInstanceForDaoPackage() {
+ Map extends Class, Object> instanceObjectMap = provider.getWrappedInstanceForDaoPackage(Lists.newArrayList(
+ "io.dropwizard.jdbi.unitofwork.core"
+ ));
+ assertEquals(2, instanceObjectMap.size());
+ assertNotNull(instanceObjectMap.get(DaoA.class));
+ assertNotNull(instanceObjectMap.get(DaoB.class));
+ assertNull(instanceObjectMap.get(DaoC.class));
+ }
+
+ interface DaoA {
+
+ @SqlUpdate
+ void update();
+ }
+
+ interface DaoB {
+
+ @SqlQuery
+ void select();
+ }
+
+ interface DaoC {
+ }
+}
diff --git a/src/test/java/io/dropwizard/jdbi/unitofwork/core/LinkedRequestScopedJdbiHandleManagerTest.java b/src/test/java/io/dropwizard/jdbi/unitofwork/core/LinkedRequestScopedJdbiHandleManagerTest.java
new file mode 100644
index 0000000..a560014
--- /dev/null
+++ b/src/test/java/io/dropwizard/jdbi/unitofwork/core/LinkedRequestScopedJdbiHandleManagerTest.java
@@ -0,0 +1,114 @@
+package io.dropwizard.jdbi.unitofwork.core;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.stubbing.Answer;
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.Handle;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class LinkedRequestScopedJdbiHandleManagerTest {
+
+ private DBI dbi;
+
+ private LinkedRequestScopedJdbiHandleManager manager;
+
+ @BeforeEach
+ public void setUp() {
+ dbi = mock(DBI.class);
+ this.manager = new LinkedRequestScopedJdbiHandleManager(dbi);
+ }
+
+ @Test
+ public void testGetSetsSameHandleForMultipleInvocationsInSameThread() {
+ when(dbi.open()).thenAnswer((Answer) invocation -> mock(Handle.class));
+ Handle firstHandle = manager.get();
+ Handle secondHandle = manager.get();
+ assertEquals(firstHandle, secondHandle);
+
+ verify(dbi, times(1)).open();
+ }
+
+ @Test
+ public void testGetSetsNewHandleForEachThread() throws InterruptedException {
+ when(dbi.open()).thenAnswer((Answer) invocation -> mock(Handle.class));
+ Handle handleThreadA = manager.get();
+
+ Thread newHandleInvokerThread = new Thread(() -> assertNotEquals(handleThreadA, manager.get()));
+ newHandleInvokerThread.start();
+ newHandleInvokerThread.join();
+ verify(dbi, times(2)).open();
+ }
+
+ @Test
+ public void testGetSetsSameHandleForChildThreadsIfTheThreadFactoryIsPlaced() throws InterruptedException {
+ when(dbi.open()).thenAnswer((Answer) invocation -> mock(Handle.class));
+ Handle parentHandle = manager.get();
+ ThreadFactory threadFactory = manager.createThreadFactory();
+
+ final int NUM_THREADS = 6;
+ CountDownLatch endGate = new CountDownLatch(NUM_THREADS);
+ ExecutorService service = Executors.newFixedThreadPool(NUM_THREADS, threadFactory);
+
+ for (int i = 0; i < NUM_THREADS; i++) {
+ service.submit(() -> {
+ Handle childHandle = manager.get();
+ assertEquals(parentHandle, childHandle);
+ endGate.countDown();
+ });
+ }
+ service.shutdown();
+ endGate.await();
+ verify(dbi, times(1)).open();
+ }
+
+ @Test
+ public void testGetSetsNewHandleForChildThreadsIfTheThreadFactoryIsNotPlaced() throws InterruptedException {
+ when(dbi.open()).thenAnswer((Answer) invocation -> mock(Handle.class));
+ Handle parentHandle = manager.get();
+
+ final int NUM_THREADS = 5;
+ CountDownLatch endGate = new CountDownLatch(NUM_THREADS);
+ ExecutorService service = Executors.newFixedThreadPool(NUM_THREADS);
+
+ for (int i = 0; i < NUM_THREADS; i++) {
+ service.submit(() -> {
+ Handle childHandle = manager.get();
+ assertNotEquals(parentHandle, childHandle);
+ endGate.countDown();
+ });
+ }
+ service.shutdown();
+ endGate.await();
+ verify(dbi, times(NUM_THREADS + 1)).open();
+ }
+
+ @Test
+ public void testClearClosesHandleAndClearsHandle() {
+ Handle mockHandle = mock(Handle.class);
+ when(dbi.open()).thenReturn(mockHandle);
+
+ manager.get();
+ manager.clear();
+ verify(dbi, times(1)).open();
+ verify(mockHandle, times(1)).close();
+ }
+
+ @Test
+ public void testClearDoesNothingWhenHandleIsNull() {
+ manager.clear();
+ verify(dbi, never()).open();
+ }
+}
diff --git a/src/test/java/io/dropwizard/jdbi/unitofwork/core/ManagedHandleInvocationHandlerTest.java b/src/test/java/io/dropwizard/jdbi/unitofwork/core/ManagedHandleInvocationHandlerTest.java
new file mode 100644
index 0000000..0fbcf3b
--- /dev/null
+++ b/src/test/java/io/dropwizard/jdbi/unitofwork/core/ManagedHandleInvocationHandlerTest.java
@@ -0,0 +1,69 @@
+package io.dropwizard.jdbi.unitofwork.core;
+
+import com.google.common.reflect.Reflection;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.skife.jdbi.v2.Handle;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@SuppressWarnings({"UnstableApiUsage"})
+public class ManagedHandleInvocationHandlerTest {
+
+ private JdbiHandleManager handleManager;
+
+ private Handle mockHandle;
+
+ DummyDao proxiedDao;
+
+ @BeforeEach
+ public void setUp() {
+ handleManager = mock(JdbiHandleManager.class);
+ mockHandle = mock(Handle.class);
+ when(handleManager.get()).thenReturn(mockHandle);
+ Class declaringClass = DummyDao.class;
+ ManagedHandleInvocationHandler proxy = new ManagedHandleInvocationHandler<>(handleManager, declaringClass);
+ Object proxiedInstance = Reflection.newProxy(declaringClass, proxy);
+ when(mockHandle.attach(declaringClass)).thenReturn(new DummyDaoImpl(mockHandle));
+ proxiedDao = declaringClass.cast(proxiedInstance);
+ }
+
+ @Test
+ public void testHandleIsAttachedInTheProxyClassAndIsCalled() {
+ proxiedDao.query();
+ verify(mockHandle, times(1)).select(any());
+ verify(mockHandle, times(1)).attach(any());
+ }
+
+ @Test
+ public void testToStringCallsTheInstanceMethodAndNotTheProxyMethod() {
+ String str = proxiedDao.toString();
+ assertEquals("Proxy[DummyDao]", str);
+ verify(handleManager, never()).get();
+ }
+
+ interface DummyDao {
+ void query();
+ }
+
+ class DummyDaoImpl implements DummyDao {
+
+ private final Handle handle;
+
+ DummyDaoImpl(Handle handle) {
+ this.handle = handle;
+ }
+
+ @Override
+ public void query() {
+ handle.select("select * from some_table");
+ assertEquals(handle, mockHandle);
+ }
+ }
+}
diff --git a/src/test/java/io/dropwizard/jdbi/unitofwork/core/RequestScopedJdbiHandleManagerTest.java b/src/test/java/io/dropwizard/jdbi/unitofwork/core/RequestScopedJdbiHandleManagerTest.java
new file mode 100644
index 0000000..0e0b90a
--- /dev/null
+++ b/src/test/java/io/dropwizard/jdbi/unitofwork/core/RequestScopedJdbiHandleManagerTest.java
@@ -0,0 +1,72 @@
+package io.dropwizard.jdbi.unitofwork.core;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.stubbing.Answer;
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.Handle;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class RequestScopedJdbiHandleManagerTest {
+
+ private DBI dbi;
+
+ private RequestScopedJdbiHandleManager manager;
+
+ @BeforeEach
+ public void setUp() {
+ dbi = mock(DBI.class);
+ this.manager = new RequestScopedJdbiHandleManager(dbi);
+ }
+
+ @Test
+ public void testGetSetsSameHandleForMultipleInvocationsInSameThread() {
+ when(dbi.open()).thenAnswer((Answer) invocation -> mock(Handle.class));
+ Handle firstHandle = manager.get();
+ Handle secondHandle = manager.get();
+ assertEquals(firstHandle, secondHandle);
+
+ verify(dbi, times(1)).open();
+ }
+
+ @Test
+ public void testGetSetsNewHandleForEachThread() throws InterruptedException {
+ when(dbi.open()).thenAnswer((Answer) invocation -> mock(Handle.class));
+ Handle handleThreadA = manager.get();
+
+ Thread newHandleInvokerThread = new Thread(() -> assertNotEquals(handleThreadA, manager.get()));
+ newHandleInvokerThread.start();
+ newHandleInvokerThread.join();
+ verify(dbi, times(2)).open();
+ }
+
+ @Test
+ public void testClearClosesHandleAndClearsThreadLocal() {
+ Handle mockHandle = mock(Handle.class);
+ when(dbi.open()).thenReturn(mockHandle);
+
+ manager.get();
+ manager.clear();
+ verify(dbi, times(1)).open();
+ verify(mockHandle, times(1)).close();
+ }
+
+ @Test
+ public void testClearDoesNothingWhenHandleIsNull() {
+ manager.clear();
+ verify(dbi, never()).open();
+ }
+
+ @Test
+ public void testCreateThreadFactoryIsNotSupported() {
+ assertThrows(UnsupportedOperationException.class, () -> manager.createThreadFactory());
+ }
+}
diff --git a/src/test/java/io/dropwizard/jdbi/unitofwork/listener/HttpGetRequestJdbiUnitOfWorkEventListenerTest.java b/src/test/java/io/dropwizard/jdbi/unitofwork/listener/HttpGetRequestJdbiUnitOfWorkEventListenerTest.java
new file mode 100644
index 0000000..fb78aae
--- /dev/null
+++ b/src/test/java/io/dropwizard/jdbi/unitofwork/listener/HttpGetRequestJdbiUnitOfWorkEventListenerTest.java
@@ -0,0 +1,38 @@
+package io.dropwizard.jdbi.unitofwork.listener;
+
+import io.dropwizard.jdbi.unitofwork.core.JdbiHandleManager;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.skife.jdbi.v2.Handle;
+
+import static org.glassfish.jersey.server.monitoring.RequestEvent.Type.FINISHED;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class HttpGetRequestJdbiUnitOfWorkEventListenerTest {
+
+ private JdbiHandleManager handleManager;
+
+ private RequestEvent requestEvent;
+
+ private HttpGetRequestJdbiUnitOfWorkEventListener listener;
+
+ @BeforeEach
+ public void setUp() {
+ handleManager = mock(JdbiHandleManager.class);
+ when(handleManager.get()).thenReturn(mock(Handle.class));
+ requestEvent = mock(RequestEvent.class);
+ this.listener = new HttpGetRequestJdbiUnitOfWorkEventListener(handleManager);
+ }
+
+ @Test
+ public void testHandleIsClosedWhenEventTypeIsFinished() {
+ when(requestEvent.getType()).thenReturn(FINISHED);
+
+ listener.onEvent(requestEvent);
+ verify(handleManager, times(1)).clear();
+ }
+}
diff --git a/src/test/java/io/dropwizard/jdbi/unitofwork/listener/JdbiTransactionAspectTest.java b/src/test/java/io/dropwizard/jdbi/unitofwork/listener/JdbiTransactionAspectTest.java
new file mode 100644
index 0000000..a712379
--- /dev/null
+++ b/src/test/java/io/dropwizard/jdbi/unitofwork/listener/JdbiTransactionAspectTest.java
@@ -0,0 +1,104 @@
+package io.dropwizard.jdbi.unitofwork.listener;
+
+import io.dropwizard.jdbi.unitofwork.core.JdbiHandleManager;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.skife.jdbi.v2.Handle;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class JdbiTransactionAspectTest {
+
+ private JdbiHandleManager handleManager;
+
+ private Handle mockHandle;
+
+ private JdbiTransactionAspect aspect;
+
+ @BeforeEach
+ public void setUp() {
+ handleManager = mock(JdbiHandleManager.class);
+ mockHandle = mock(Handle.class);
+ when(handleManager.get()).thenReturn(mockHandle);
+ this.aspect = new JdbiTransactionAspect(handleManager);
+ }
+
+ @Test
+ public void testBeginWhenHandleBeginThrowsException() {
+ when(mockHandle.begin()).thenThrow(IllegalArgumentException.class);
+ assertThrows(IllegalArgumentException.class, () -> aspect.begin());
+ verify(handleManager, times(1)).clear();
+ verify(mockHandle, never()).commit();
+ }
+
+ @Test
+ public void testBeginWorksAsExpected() {
+ doReturn(mockHandle).when(mockHandle).begin();
+ aspect.begin();
+
+ verify(mockHandle, times(1)).begin();
+ verify(mockHandle, never()).close();
+ verify(handleManager, never()).clear();
+ verify(mockHandle, never()).commit();
+ }
+
+ @Test
+ public void testCommitDoesNothingWhenHandleIsNull() {
+ assertDoesNotThrow(() -> aspect.commit());
+ }
+
+ @Test
+ public void testCommitWhenHandleCommitThrowsException() {
+ when(mockHandle.commit()).thenThrow(IllegalArgumentException.class);
+ assertThrows(IllegalArgumentException.class, () -> aspect.commit());
+ verify(mockHandle, times(1)).rollback();
+ }
+
+ @Test
+ public void testCommitWorksAsExpected() {
+ doReturn(mockHandle).when(mockHandle).commit();
+ aspect.commit();
+
+ verify(mockHandle, times(1)).commit();
+ verify(mockHandle, never()).rollback();
+ }
+
+ @Test
+ public void testRollbackDoesNothingWhenHandleIsNull() {
+ assertDoesNotThrow(() -> aspect.rollback());
+ }
+
+ @Test
+ public void testRollbackWhenHandleRollbackThrowsException() {
+ when(mockHandle.rollback()).thenThrow(IllegalArgumentException.class);
+ assertThrows(IllegalArgumentException.class, () -> aspect.rollback());
+ verify(mockHandle, times(1)).rollback();
+ }
+
+ @Test
+ public void testRollbackWorksAsExpected() {
+ doReturn(mockHandle).when(mockHandle).rollback();
+ aspect.rollback();
+
+ verify(mockHandle, times(1)).rollback();
+ verify(handleManager, times(1)).clear();
+ }
+
+ @Test
+ public void testTerminateHandleDoesNothingWhenHandleIsNull() {
+ assertDoesNotThrow(() -> aspect.terminateHandle());
+ }
+
+ @Test
+ public void testTerminateHandleWorksAsExpected() {
+ aspect.terminateHandle();
+ verify(handleManager, times(1)).clear();
+ }
+}
diff --git a/src/test/java/io/dropwizard/jdbi/unitofwork/listener/JdbiUnitOfWorkApplicationEventListenerTest.java b/src/test/java/io/dropwizard/jdbi/unitofwork/listener/JdbiUnitOfWorkApplicationEventListenerTest.java
new file mode 100644
index 0000000..c266e22
--- /dev/null
+++ b/src/test/java/io/dropwizard/jdbi/unitofwork/listener/JdbiUnitOfWorkApplicationEventListenerTest.java
@@ -0,0 +1,75 @@
+package io.dropwizard.jdbi.unitofwork.listener;
+
+import com.google.common.collect.Sets;
+import io.dropwizard.jdbi.unitofwork.core.JdbiHandleManager;
+import io.dropwizard.jdbi.unitofwork.core.JdbiUnitOfWorkProvider;
+import org.glassfish.jersey.server.monitoring.ApplicationEvent;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEventListener;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.skife.jdbi.v2.Handle;
+
+import javax.ws.rs.HttpMethod;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class JdbiUnitOfWorkApplicationEventListenerTest {
+
+ private RequestEvent requestEvent;
+
+ private JdbiUnitOfWorkApplicationEventListener applicationListener;
+
+ @BeforeEach
+ public void setUp() {
+ JdbiHandleManager handleManager = mock(JdbiHandleManager.class);
+ when(handleManager.get()).thenReturn(mock(Handle.class));
+ JdbiUnitOfWorkProvider provider = mock(JdbiUnitOfWorkProvider.class);
+ when(provider.getHandleManager()).thenReturn(handleManager);
+
+ requestEvent = mock(RequestEvent.class, Mockito.RETURNS_DEEP_STUBS);
+ Set excludedPaths = Sets.newHashSet("excluded");
+ this.applicationListener = new JdbiUnitOfWorkApplicationEventListener(provider, excludedPaths);
+ }
+
+ @Test
+ public void testOnEventDoesNothing() {
+ ApplicationEvent applicationEvent = mock(ApplicationEvent.class);
+ applicationListener.onEvent(applicationEvent);
+ verify(applicationEvent, times(1)).getType();
+ }
+
+ @Test
+ public void testOnRequestDoesNothingWhenRequestEventPathIsExcluded() {
+ when(requestEvent.getUriInfo().getPath()).thenReturn("excluded");
+ assertNull(applicationListener.onRequest(requestEvent));
+ }
+
+ @Test
+ public void testOnRequestReturnsCorrectEventListenerWhenMethodTypeIsGet() {
+ when(requestEvent.getUriInfo().getPath()).thenReturn("exclude-me-not");
+ when(requestEvent.getContainerRequest().getMethod()).thenReturn(HttpMethod.GET);
+
+ RequestEventListener eventListener = applicationListener.onRequest(requestEvent);
+ assertNotNull(eventListener);
+ assertEquals(HttpGetRequestJdbiUnitOfWorkEventListener.class, eventListener.getClass());
+ }
+
+ @Test
+ public void testOnRequestReturnsCorrectEventListenerWhenMethodTypeIsNotGet() {
+ when(requestEvent.getUriInfo().getPath()).thenReturn("exclude-me-not");
+ when(requestEvent.getContainerRequest().getMethod()).thenReturn(HttpMethod.PUT);
+
+ RequestEventListener eventListener = applicationListener.onRequest(requestEvent);
+ assertNotNull(eventListener);
+ assertEquals(NonHttpGetRequestJdbiUnitOfWorkEventListener.class, eventListener.getClass());
+ }
+}
diff --git a/src/test/java/io/dropwizard/jdbi/unitofwork/listener/NonHttpGetRequestJdbiUnitOfWorkEventListenerTest.java b/src/test/java/io/dropwizard/jdbi/unitofwork/listener/NonHttpGetRequestJdbiUnitOfWorkEventListenerTest.java
new file mode 100644
index 0000000..67a343e
--- /dev/null
+++ b/src/test/java/io/dropwizard/jdbi/unitofwork/listener/NonHttpGetRequestJdbiUnitOfWorkEventListenerTest.java
@@ -0,0 +1,141 @@
+package io.dropwizard.jdbi.unitofwork.listener;
+
+import io.dropwizard.jdbi.unitofwork.JdbiUnitOfWork;
+import io.dropwizard.jdbi.unitofwork.core.JdbiHandleManager;
+import org.glassfish.jersey.server.model.Resource;
+import org.glassfish.jersey.server.model.ResourceMethod;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.skife.jdbi.v2.Handle;
+
+import javax.ws.rs.core.MediaType;
+
+import static org.glassfish.jersey.server.monitoring.RequestEvent.Type.FINISHED;
+import static org.glassfish.jersey.server.monitoring.RequestEvent.Type.ON_EXCEPTION;
+import static org.glassfish.jersey.server.monitoring.RequestEvent.Type.RESOURCE_METHOD_START;
+import static org.glassfish.jersey.server.monitoring.RequestEvent.Type.RESP_FILTERS_START;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+public class NonHttpGetRequestJdbiUnitOfWorkEventListenerTest {
+
+ private JdbiHandleManager handleManager;
+
+ private Handle handle;
+
+ private RequestEvent requestEvent;
+
+ private NonHttpGetRequestJdbiUnitOfWorkEventListener listener;
+
+ @BeforeEach
+ public void setUp() {
+ handleManager = mock(JdbiHandleManager.class);
+ handle = mock(Handle.class);
+ when(handleManager.get()).thenReturn(handle);
+ requestEvent = mock(RequestEvent.class, Mockito.RETURNS_DEEP_STUBS);
+ when(requestEvent.getContainerRequest().getMethod()).thenReturn("PUT");
+ this.listener = new NonHttpGetRequestJdbiUnitOfWorkEventListener(handleManager);
+ }
+
+ @Test
+ public void testHandleIsNoOpWhenEventTypeIsResourceMethodStartButNotTransactional() {
+ when(requestEvent.getType()).thenReturn(RESOURCE_METHOD_START);
+ when(requestEvent.getUriInfo().getMatchedResourceMethod()).thenReturn(null);
+
+ listener.onEvent(requestEvent);
+ verifyNoInteractions(handleManager);
+ }
+
+ @Test
+ public void testHandleIsNotCommittedWhenEventTypeIsRespFilterStartButNotTransactional() {
+ when(requestEvent.getType()).thenReturn(RESP_FILTERS_START);
+ when(requestEvent.getUriInfo().getMatchedResourceMethod()).thenReturn(null);
+
+ listener.onEvent(requestEvent);
+ verify(handleManager, never()).get();
+ }
+
+ @Test
+ public void testHandleIsNotRolledBackWhenEventTypeIsOnExceptionButNotTransactional() {
+ when(requestEvent.getType()).thenReturn(ON_EXCEPTION);
+ when(requestEvent.getUriInfo().getMatchedResourceMethod()).thenReturn(null);
+
+ listener.onEvent(requestEvent);
+ verify(handleManager, never()).get();
+ }
+
+ @Test
+ public void testHandleIsTerminatedWhenEventTypeIsResourceMethodStartButNotTransactional() {
+ when(requestEvent.getType()).thenReturn(RESOURCE_METHOD_START).thenReturn(FINISHED);
+ when(requestEvent.getUriInfo().getMatchedResourceMethod()).thenReturn(null);
+
+ listener.onEvent(requestEvent);
+
+ listener.onEvent(requestEvent);
+ verify(handleManager, times(1)).clear();
+ }
+
+ @Test
+ public void testHandleIsClosedWhenEventTypeIsFinished() {
+ when(requestEvent.getType()).thenReturn(FINISHED);
+ listener.onEvent(requestEvent);
+ verify(handleManager, times(1)).clear();
+ }
+
+ @Test
+ public void testHandleIsInitialisedWhenEventTypeIsResourceMethodStartTransactional() throws NoSuchMethodException {
+ when(requestEvent.getType()).thenReturn(RESOURCE_METHOD_START);
+ when(requestEvent.getUriInfo().getMatchedResourceMethod()).thenReturn(getMockResourceMethod());
+
+ listener.onEvent(requestEvent);
+ verify(handleManager, times(1)).get();
+ verify(handle, times(1)).begin();
+ }
+
+ @Test
+ public void testHandleIsNotCommittedWhenEventTypeIsRespFilterStartTransactional() throws NoSuchMethodException {
+ when(requestEvent.getType()).thenReturn(RESOURCE_METHOD_START).thenReturn(RESP_FILTERS_START);
+ when(requestEvent.getUriInfo().getMatchedResourceMethod()).thenReturn(getMockResourceMethod());
+
+ listener.onEvent(requestEvent);
+ verify(handleManager, times(1)).get();
+
+ listener.onEvent(requestEvent);
+ verify(handle, times(1)).commit();
+ }
+
+ @Test
+ public void testHandleIsNotRolledBackWhenEventTypeIsOnExceptionTransactional() throws NoSuchMethodException {
+ when(requestEvent.getType()).thenReturn(RESOURCE_METHOD_START).thenReturn(ON_EXCEPTION);
+ when(requestEvent.getUriInfo().getMatchedResourceMethod()).thenReturn(getMockResourceMethod());
+
+ listener.onEvent(requestEvent);
+ verify(handleManager, times(1)).get();
+
+ listener.onEvent(requestEvent);
+ verify(handle, times(1)).rollback();
+ }
+
+ private ResourceMethod getMockResourceMethod() throws NoSuchMethodException {
+ return Resource
+ .builder()
+ .addMethod("PUT")
+ .produces(MediaType.TEXT_PLAIN_TYPE)
+ .handledBy(ResourceMethodStub.class, ResourceMethodStub.class.getMethod("apply"))
+ .build();
+ }
+
+ static class ResourceMethodStub {
+
+ @JdbiUnitOfWork
+ public String apply() {
+ return "";
+ }
+ }
+}