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 factory) { + return (T) Proxy.newProxyInstance(jdbcMapper.getClassLoader(), + new Class[]{jdbcMapper}, new SingletonCloseable(factory) + ); + } + + protected final Factory factory; + + protected SingletonCloseable(final Factory 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 factory) { + return (T) Proxy.newProxyInstance(jdbcMapper.getClassLoader(), + new Class[]{jdbcMapper}, new SingletonCloseable(factory) + ); + } + + protected final Factory factory; + + protected SingletonCloseable(final Factory 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); + } + } +}