diff --git a/common/src/main/java/com/moparisthebest/jdbc/SingletonCloseable.java b/common/src/main/java/com/moparisthebest/jdbc/SingletonCloseable.java
new file mode 100644
index 0000000..706d27e
--- /dev/null
+++ b/common/src/main/java/com/moparisthebest/jdbc/SingletonCloseable.java
@@ -0,0 +1,130 @@
+package com.moparisthebest.jdbc;
+
+import com.moparisthebest.jdbc.codegen.JdbcMapperFactory;
+
+import java.io.Closeable;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.sql.ResultSet;
+
+import static com.moparisthebest.jdbc.TryClose.tryClose;
+
+/**
+ * This provides implementations of Closeable/JdbcMapper interfaces that simply construct a new object,
+ * execute the method, and close the object for each method call. Implementations returned never need
+ * to be closed and are as thread-safe as the Factory sent in, of which the provided impls are entirely thread-safe.
+ *
+ * If any of the methods returns an *interface* that extends Closeable, we assume *this* Closeable should not be closed
+ * until the returned value is, and it is therefore wrapped with WrappingCloseable before return
+ */
+public class SingletonCloseable implements InvocationHandler {
+
+ //IFJAVA8_START
+
+ public static T of(final Class jdbcMapper) {
+ return of(jdbcMapper, JdbcMapperFactory.of(jdbcMapper));
+ }
+
+ public static T of(final Class jdbcMapper, final String jndiName) {
+ return of(jdbcMapper, JdbcMapperFactory.of(jdbcMapper, jndiName));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static T of(final Class jdbcMapper, final Factory extends T> factory) {
+ return (T) Proxy.newProxyInstance(jdbcMapper.getClassLoader(),
+ new Class>[]{jdbcMapper}, new SingletonCloseable(factory)
+ );
+ }
+
+ protected final Factory extends AutoCloseable> factory;
+
+ protected SingletonCloseable(final Factory extends AutoCloseable> factory) {
+ this.factory = factory;
+ }
+
+ @Override
+ public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
+ AutoCloseable jdbcMapper = null;
+ try {
+ jdbcMapper = factory.create();
+ Object ret = method.invoke(jdbcMapper, args);
+ if (ret instanceof AutoCloseable) {
+ // wrap jdbcMapper into ret so it gets closed when ret does, can only do this for Interfaces like ResultSet not concrete classes
+ final Class> returnType = method.getReturnType();
+ if (returnType.isInterface() && AutoCloseable.class.isAssignableFrom(returnType)) {
+ @SuppressWarnings("unchecked")
+ final Class returnTypeCloseable = (Class) returnType;
+ ret = WrappingCloseable.wrap((AutoCloseable) ret, returnTypeCloseable, jdbcMapper);
+ // now that jdbcMapper will be closed by above, don't let it be closed here
+ // if a ClassCastException might happen here we will leak jdbcMapper, but how could it???
+ jdbcMapper = null;
+ }
+ }
+ return ret;
+ } finally {
+ tryClose(jdbcMapper);
+ }
+ }
+
+ //IFJAVA8_END
+
+ /*IFJAVA6_START
+
+ public static T of(final Class jdbcMapper) {
+ return of(jdbcMapper, JdbcMapperFactory.of(jdbcMapper));
+ }
+
+ public static T of(final Class jdbcMapper, final String jndiName) {
+ return of(jdbcMapper, JdbcMapperFactory.of(jdbcMapper, jndiName));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static T of(final Class jdbcMapper, final Factory extends T> factory) {
+ return (T) Proxy.newProxyInstance(jdbcMapper.getClassLoader(),
+ new Class>[]{jdbcMapper}, new SingletonCloseable(factory)
+ );
+ }
+
+ protected final Factory extends Closeable> factory;
+
+ protected SingletonCloseable(final Factory extends Closeable> factory) {
+ this.factory = factory;
+ }
+
+ @Override
+ public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
+ Closeable jdbcMapper = null;
+ try {
+ jdbcMapper = factory.create();
+ Object ret = method.invoke(jdbcMapper, args);
+ if (ret instanceof Closeable) {
+ // wrap jdbcMapper into ret so it gets closed when ret does, can only do this for Interfaces like ResultSet not concrete classes
+ final Class> returnType = method.getReturnType();
+ if (returnType.isInterface() && Closeable.class.isAssignableFrom(returnType)) {
+ @SuppressWarnings("unchecked") final Class returnTypeCloseable = (Class) returnType;
+ ret = WrappingCloseable.wrap((Closeable) ret, returnTypeCloseable, jdbcMapper);
+ // now that jdbcMapper will be closed by above, don't let it be closed here
+ // if a ClassCastException might happen here we will leak jdbcMapper, but how could it???
+ jdbcMapper = null;
+ }
+ } else if (ret instanceof ResultSet) {
+ // wrap jdbcMapper into ret so it gets closed when ret does, can only do this for Interfaces like ResultSet not concrete classes
+ final Class> returnType = method.getReturnType();
+ if (returnType.isInterface() && ResultSet.class.isAssignableFrom(returnType)) {
+ @SuppressWarnings("unchecked") final Class returnTypeCloseable = (Class) returnType;
+ ret = WrappingCloseable.wrap((ResultSet) ret, returnTypeCloseable, jdbcMapper);
+ // now that jdbcMapper will be closed by above, don't let it be closed here
+ // if a ClassCastException might happen here we will leak jdbcMapper, but how could it???
+ jdbcMapper = null;
+ }
+ }
+ return ret;
+ } finally {
+ tryClose(jdbcMapper);
+ }
+ }
+
+ IFJAVA6_END*/
+
+}
diff --git a/common/src/main/java/com/moparisthebest/jdbc/WrappingCloseable.java b/common/src/main/java/com/moparisthebest/jdbc/WrappingCloseable.java
new file mode 100644
index 0000000..d1922ff
--- /dev/null
+++ b/common/src/main/java/com/moparisthebest/jdbc/WrappingCloseable.java
@@ -0,0 +1,76 @@
+package com.moparisthebest.jdbc;
+
+import java.io.Closeable;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.sql.ResultSet;
+
+import static com.moparisthebest.jdbc.TryClose.tryClose;
+
+/**
+ * This wraps a class implementing closeable and also closes a supplied closeable after this class is closed
+ *
+ * An example use is for holding a Connection open while a returned ResultSet is still held open, and closing the
+ * Connection when the ResultSet is closed
+ */
+public class WrappingCloseable implements InvocationHandler {
+
+ //IFJAVA8_START
+
+ @SuppressWarnings("unchecked")
+ public static T wrap(final T delegate, final Class iface, final C closeable) {
+ return (T) Proxy.newProxyInstance(iface.getClassLoader(),
+ new Class>[]{iface}, new WrappingCloseable(delegate, closeable)
+ );
+ }
+
+ protected final Object delegate;
+ protected final AutoCloseable closeable;
+
+ protected WrappingCloseable(final Object delegate, final AutoCloseable closeable) {
+ this.delegate = delegate;
+ this.closeable = closeable;
+ }
+
+ //IFJAVA8_END
+
+ /*IFJAVA6_START
+
+ @SuppressWarnings("unchecked")
+ public static T wrap(final T delegate, final Class iface, final C closeable) {
+ return (T) Proxy.newProxyInstance(iface.getClassLoader(),
+ new Class>[]{iface}, new WrappingCloseable(delegate, closeable)
+ );
+ }
+
+ @SuppressWarnings("unchecked")
+ public static T wrap(final T delegate, final Class iface, final C closeable) {
+ return (T) Proxy.newProxyInstance(iface.getClassLoader(),
+ new Class>[]{iface}, new WrappingCloseable(delegate, closeable)
+ );
+ }
+
+ protected final Object delegate;
+ protected final Closeable closeable;
+
+ protected WrappingCloseable(final Object delegate, final Closeable closeable) {
+ this.delegate = delegate;
+ this.closeable = closeable;
+ }
+
+ IFJAVA6_END*/
+
+ @Override
+ public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
+ try {
+ return method.invoke(delegate, args);
+ } finally {
+ // todo: is there a better way of determining close method?
+ if ((args == null || args.length == 0) && "close".equals(method.getName())) {
+ tryClose(closeable);
+ }
+ }
+ }
+
+}
diff --git a/common/src/test/java/com/moparisthebest/jdbc/Closed.java b/common/src/test/java/com/moparisthebest/jdbc/Closed.java
new file mode 100644
index 0000000..e789ace
--- /dev/null
+++ b/common/src/test/java/com/moparisthebest/jdbc/Closed.java
@@ -0,0 +1,10 @@
+package com.moparisthebest.jdbc;
+
+import java.io.Closeable;
+
+interface Closed extends Closeable {
+
+ boolean isClosed();
+
+ String name();
+}
diff --git a/common/src/test/java/com/moparisthebest/jdbc/SimpleCloseable.java b/common/src/test/java/com/moparisthebest/jdbc/SimpleCloseable.java
new file mode 100644
index 0000000..b327a6d
--- /dev/null
+++ b/common/src/test/java/com/moparisthebest/jdbc/SimpleCloseable.java
@@ -0,0 +1,27 @@
+package com.moparisthebest.jdbc;
+
+class SimpleCloseable implements Closed {
+
+ final String name;
+
+ private boolean closed = false;
+
+ SimpleCloseable(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean isClosed() {
+ return closed;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public void close() {
+ closed = true;
+ }
+}
diff --git a/common/src/test/java/com/moparisthebest/jdbc/SingletonCloseableTest.java b/common/src/test/java/com/moparisthebest/jdbc/SingletonCloseableTest.java
new file mode 100644
index 0000000..02dde19
--- /dev/null
+++ b/common/src/test/java/com/moparisthebest/jdbc/SingletonCloseableTest.java
@@ -0,0 +1,198 @@
+package com.moparisthebest.jdbc;
+
+import org.junit.Test;
+
+import java.io.Closeable;
+import java.sql.SQLException;
+
+import static org.junit.Assert.*;
+
+public class SingletonCloseableTest {
+
+ @Test
+ public void testRegularCloseable() throws Exception {
+ final DaoFactory factory = new DaoFactory();
+ assertEquals(0, factory.totalOpened);
+ assertEquals(0, factory.numStillOpen);
+
+ {
+ final Dao dao1 = factory.create();
+ assertEquals(1, factory.totalOpened);
+ assertEquals(1, factory.numStillOpen);
+
+ final Closed closed1 = dao1.getClosed();
+ assertEquals(1, factory.totalOpened);
+ assertEquals(1, factory.numStillOpen);
+ assertEquals("num1", closed1.name());
+ closed1.close();
+ assertTrue(closed1.isClosed());
+ assertEquals(1, factory.totalOpened);
+ assertEquals(1, factory.numStillOpen);
+
+ final Closed closed2 = dao1.getClosed();
+ assertEquals(1, factory.totalOpened);
+ assertEquals(1, factory.numStillOpen);
+ assertEquals("num2", closed2.name());
+ closed2.close();
+ assertTrue(closed2.isClosed());
+ assertEquals(1, factory.totalOpened);
+ assertEquals(1, factory.numStillOpen);
+
+ dao1.close();
+ assertEquals(1, factory.totalOpened);
+ assertEquals(0, factory.numStillOpen);
+ assertTrue(dao1.isClosed());
+ assertEquals(0, factory.numStillOpen);
+ }
+
+ final Dao dao1 = factory.create();
+ assertEquals(2, factory.totalOpened);
+ assertEquals(1, factory.numStillOpen);
+
+ final Closed closed1 = dao1.getClosed();
+ assertEquals(2, factory.totalOpened);
+ assertEquals(1, factory.numStillOpen);
+ assertEquals("num1", closed1.name());
+
+ final Closed closed2 = dao1.getClosed();
+ assertEquals(2, factory.totalOpened);
+ assertEquals(1, factory.numStillOpen);
+ assertEquals("num2", closed2.name());
+
+ assertEquals(5, dao1.getInt());
+ assertEquals(2, factory.totalOpened);
+ assertEquals(1, factory.numStillOpen);
+
+ dao1.close();
+ assertEquals(2, factory.totalOpened);
+ assertEquals(0, factory.numStillOpen);
+ assertTrue(dao1.isClosed());
+ assertFalse(closed1.isClosed());
+ assertFalse(closed2.isClosed());
+ }
+
+ @Test
+ public void testSingletonCloseable() throws Exception {
+ final DaoFactory factory = new DaoFactory();
+ assertEquals(0, factory.totalOpened);
+ assertEquals(0, factory.numStillOpen);
+ final Dao dao1 = SingletonCloseable.of(Dao.class, factory);
+
+ {
+ assertEquals(0, factory.totalOpened);
+ assertEquals(0, factory.numStillOpen);
+
+ final Closed closed1 = dao1.getClosed();
+ assertEquals(1, factory.totalOpened);
+ assertEquals(1, factory.numStillOpen);
+ assertEquals("num1", closed1.name());
+ closed1.close();
+ assertTrue(closed1.isClosed());
+ assertEquals(1, factory.totalOpened);
+ assertEquals(0, factory.numStillOpen);
+
+ final Closed closed2 = dao1.getClosed();
+ assertEquals(2, factory.totalOpened);
+ assertEquals(1, factory.numStillOpen);
+ assertEquals("num1", closed2.name());
+ closed2.close();
+ assertTrue(closed2.isClosed());
+ assertEquals(2, factory.totalOpened);
+ assertEquals(0, factory.numStillOpen);
+
+ dao1.close();
+ assertEquals(3, factory.totalOpened);
+ assertEquals(0, factory.numStillOpen);
+ assertFalse(dao1.isClosed()); // always appears open even though does not stay open
+ assertEquals(0, factory.numStillOpen);
+ }
+
+ assertEquals(4, factory.totalOpened);
+ assertEquals(0, factory.numStillOpen);
+
+ final Closed closed1 = dao1.getClosed();
+ assertEquals(5, factory.totalOpened);
+ assertEquals(1, factory.numStillOpen);
+ assertEquals("num1", closed1.name());
+
+ final Closed closed2 = dao1.getClosed();
+ assertEquals(6, factory.totalOpened);
+ assertEquals(2, factory.numStillOpen);
+ assertEquals("num1", closed2.name());
+
+ assertEquals(5, dao1.getInt());
+ assertEquals(7, factory.totalOpened);
+ assertEquals(2, factory.numStillOpen);
+
+ dao1.close();
+ assertEquals(8, factory.totalOpened);
+ assertEquals(2, factory.numStillOpen);
+ assertFalse(dao1.isClosed());
+ assertEquals(9, factory.totalOpened);
+ assertEquals(2, factory.numStillOpen);
+ assertFalse(closed1.isClosed());
+ assertFalse(closed2.isClosed());
+ assertEquals(9, factory.totalOpened);
+ assertEquals(2, factory.numStillOpen);
+ closed1.close();
+ assertEquals(9, factory.totalOpened);
+ assertEquals(1, factory.numStillOpen);
+ closed2.close();
+ assertEquals(9, factory.totalOpened);
+ assertEquals(0, factory.numStillOpen);
+ }
+
+ class DaoFactory implements Factory {
+ int totalOpened = 0;
+ int numStillOpen = 0;
+
+ @Override
+ public Dao create() throws SQLException {
+ return new DaoImpl(this);
+ }
+ }
+
+ class DaoImpl implements Dao {
+ final DaoFactory factory;
+
+ private int simpleCloseableCount = 0;
+ private boolean closed = false;
+
+ DaoImpl(DaoFactory factory) {
+ ++factory.totalOpened;
+ ++factory.numStillOpen;
+ this.factory = factory;
+ }
+
+ @Override
+ public boolean isClosed() {
+ return closed;
+ }
+
+ @Override
+ public int getInt() {
+ return 5;
+ }
+
+ @Override
+ public Closed getClosed() {
+ return new SimpleCloseable("num" + (++simpleCloseableCount));
+ }
+
+ @Override
+ public void close() {
+ if (!closed) {
+ --factory.numStillOpen;
+ closed = true;
+ }
+ }
+ }
+
+ interface Dao extends Closeable {
+ boolean isClosed();
+
+ int getInt();
+
+ Closed getClosed();
+ }
+}
\ No newline at end of file
diff --git a/common/src/test/java/com/moparisthebest/jdbc/WrappingCloseableTest.java b/common/src/test/java/com/moparisthebest/jdbc/WrappingCloseableTest.java
new file mode 100644
index 0000000..df653af
--- /dev/null
+++ b/common/src/test/java/com/moparisthebest/jdbc/WrappingCloseableTest.java
@@ -0,0 +1,45 @@
+package com.moparisthebest.jdbc;
+
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.*;
+
+public class WrappingCloseableTest {
+
+ @Test
+ public void test() throws IOException {
+ final Closed parent = new SimpleCloseable("parent");
+ final Closed child = new SimpleCloseable("child");
+
+ assertFalse(parent.isClosed());
+ assertEquals("parent", parent.name());
+
+ assertFalse(child.isClosed());
+ assertEquals("child", child.name());
+
+ final Closed wrapped = WrappingCloseable.wrap(child, Closed.class, parent);
+ // when wrapped is closed, it should close child and then parent
+
+ assertFalse(parent.isClosed());
+ assertEquals("parent", parent.name());
+
+ assertFalse(child.isClosed());
+ assertEquals("child", child.name());
+
+ assertFalse(wrapped.isClosed());
+ assertEquals("child", wrapped.name());
+
+ wrapped.close();
+
+ assertTrue(parent.isClosed());
+ assertEquals("parent", parent.name());
+
+ assertTrue(child.isClosed());
+ assertEquals("child", child.name());
+
+ assertTrue(wrapped.isClosed());
+ assertEquals("child", wrapped.name());
+ }
+}
\ No newline at end of file
diff --git a/test/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAOSingletonTest.java b/test/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAOSingletonTest.java
new file mode 100644
index 0000000..c77b07b
--- /dev/null
+++ b/test/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAOSingletonTest.java
@@ -0,0 +1,65 @@
+package com.moparisthebest.jdbc.codegen;
+
+import com.moparisthebest.jdbc.Factory;
+import com.moparisthebest.jdbc.QueryMapperTest;
+import com.moparisthebest.jdbc.SingletonCloseable;
+import org.junit.Test;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import static com.moparisthebest.jdbc.QueryMapperTest.fieldPerson1;
+import static com.moparisthebest.jdbc.TryClose.tryClose;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class PersonDAOSingletonTest {
+
+ public static final PersonDAO personDAO = SingletonCloseable.of(PersonDAO.class, JdbcMapperFactory.of(PersonDAO.class, new Factory() {
+ @Override
+ public Connection create() throws SQLException {
+ return QueryMapperTest.getConnection();
+ }
+ }));
+
+ @Test
+ public void testRegularPersonDAO() throws Exception {
+ PersonDAO personDAO = null;
+ try {
+ personDAO = JdbcMapperFactory.create(PersonDAO.class, new Factory() {
+ @Override
+ public Connection create() throws SQLException {
+ return QueryMapperTest.getConnection();
+ }
+ });
+ testPerson(personDAO);
+ } finally {
+ tryClose(personDAO);
+ }
+ }
+
+ @Test
+ public void testSingletonPersonDAO() throws Exception {
+ testPerson(personDAO);
+ }
+
+ private void testPerson(final PersonDAO personDAO) throws Exception {
+ assertEquals(fieldPerson1, personDAO.getPerson(fieldPerson1.getPersonNo()));
+ ResultSet rs = null;
+ try {
+ rs = personDAO.getPeopleResultSet(fieldPerson1.getLastName());
+ assertTrue(rs.next());
+ assertEquals(fieldPerson1.getLastName(), rs.getString("last_name"));
+ } finally {
+ tryClose(rs);
+ }
+ try {
+ rs = personDAO.getPeopleResultSetCached(fieldPerson1.getLastName());
+ assertTrue(rs.next());
+ assertEquals(fieldPerson1.getLastName(), rs.getString("last_name"));
+ } finally {
+ tryClose(rs);
+ }
+ }
+}