diff --git a/rxjava2-jdbc/src/main/java/org/davidmoten/rx/jdbc/Util.java b/rxjava2-jdbc/src/main/java/org/davidmoten/rx/jdbc/Util.java index 91d9ef47..25712968 100644 --- a/rxjava2-jdbc/src/main/java/org/davidmoten/rx/jdbc/Util.java +++ b/rxjava2-jdbc/src/main/java/org/davidmoten/rx/jdbc/Util.java @@ -5,10 +5,7 @@ import java.io.Reader; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.Proxy; +import java.lang.reflect.*; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Blob; @@ -719,6 +716,11 @@ static ResultSetMapper autoMap(Class cls) { @Override public T apply(ResultSet rs) { + + if (!cls.isInterface()) { + return autoMapClass(rs, cls); + } + // access to this method will be serialized // only create a new ProxyService when the ResultSet changes // for example with the second run of a PreparedStatement @@ -726,11 +728,70 @@ public T apply(ResultSet rs) { this.rs = rs; proxyService = new ProxyService(rs, cls); } + return autoMap(rs, cls, proxyService); } }; } + private static T autoMapClass(ResultSet rs, Class cls) { + try { + int n = rs.getMetaData().getColumnCount(); + for (Constructor c : cls.getDeclaredConstructors()) { + if (n == c.getParameterTypes().length) { + return autoMap(rs, (Constructor) c); + } + } + throw new RuntimeException("constructor with number of parameters=" + n + " not found in " + cls); + } catch (SQLException e) { + throw new SQLRuntimeException(e); + } + } + + /** + * Converts the ResultSet column values into parameters to the given + * constructor (with number of parameters equals the number of columns) of + * type T then returns an instance of type T. + * + * @param rs + * the result set row + * @param c + * constructor to use for instantiation + * @return automapped instance + */ + private static T autoMap(ResultSet rs, Constructor c) { + Class[] types = c.getParameterTypes(); + List list = new ArrayList(); + for (int i = 0; i < types.length; i++) { + list.add(autoMap(getObject(rs, types[i], i + 1), types[i])); + } + try { + return newInstance(c, list); + } catch (RuntimeException e) { + throw new RuntimeException( + "problem with parameters=" + getTypeInfo(list) + ", rs types=" + getRowInfo(rs) + + ". Be sure not to use primitives in a constructor when calling autoMap().", + e); + } + } + + /** + * Creates a new instance of type T, by invoking the provided constructor with the parameters + * @param c + * constructor to use + * @param parameters + * constructor parameters + * @return new instance of type T + */ + @SuppressWarnings("unchecked") + private static T newInstance(Constructor c, List parameters) { + try { + return (T) c.newInstance(parameters.toArray()); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + /** * Converts the ResultSet column values into parameters to the constructor (with * number of parameters equals the number of columns) of type T @@ -1009,6 +1070,48 @@ static String camelCaseToUnderscore(String camelCased) { return camelCased.replaceAll(regex, replacement); } + /** + * Returns debugging info about the types of a list of objects. + * + * @param list + * @return + */ + private static String getTypeInfo(List list) { + + StringBuilder s = new StringBuilder(); + for (Object o : list) { + if (s.length() > 0) + s.append(", "); + if (o == null) + s.append("null"); + else { + s.append(o.getClass().getName()); + s.append("="); + s.append(o); + } + } + return s.toString(); + } + + private static String getRowInfo(ResultSet rs) { + StringBuilder s = new StringBuilder(); + try { + ResultSetMetaData md = rs.getMetaData(); + for (int i = 1; i <= md.getColumnCount(); i++) { + String name = md.getColumnName(i); + String type = md.getColumnClassName(i); + if (s.length() > 0) + s.append(", "); + s.append(name); + s.append("="); + s.append(type); + } + } catch (SQLException e1) { + throw new SQLRuntimeException(e1); + } + return s.toString(); + } + public static ConnectionProvider connectionProvider(String url, Properties properties) { return new ConnectionProvider() { diff --git a/rxjava2-jdbc/src/test/java/org/davidmoten/rx/jdbc/UtilTest.java b/rxjava2-jdbc/src/test/java/org/davidmoten/rx/jdbc/UtilTest.java index a740ee1f..68648ec6 100644 --- a/rxjava2-jdbc/src/test/java/org/davidmoten/rx/jdbc/UtilTest.java +++ b/rxjava2-jdbc/src/test/java/org/davidmoten/rx/jdbc/UtilTest.java @@ -1,5 +1,7 @@ package org.davidmoten.rx.jdbc; +import static org.davidmoten.rx.jdbc.fixtures.Fixtures.listOf; +import static org.davidmoten.rx.jdbc.fixtures.Fixtures.mockPersonResultSet; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -11,17 +13,14 @@ import java.io.Reader; import java.lang.reflect.Method; import java.math.BigInteger; -import java.sql.Blob; -import java.sql.Clob; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; +import java.sql.*; import java.time.Instant; import java.util.List; import javax.sql.DataSource; import org.davidmoten.rx.jdbc.exceptions.SQLRuntimeException; +import org.davidmoten.rx.jdbc.fixtures.Person; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runners.MethodSorters; @@ -76,6 +75,24 @@ public void testAutomapDateToString() { assertEquals(100L, ((java.sql.Date) Util.autoMap(new java.sql.Date(100), String.class)).getTime()); } + @Test(expected = RuntimeException.class) + public void testClassAutoMap() throws SQLException { + ResultSetMapper mapper = Util.autoMap(Person.class); + + ResultSet mockedResultSet = mockPersonResultSet(listOf("John", "Doe")); + Person mappedPerson = mapper.apply(mockedResultSet); + + assertEquals(mappedPerson, new Person("John", "Doe")); + + mockedResultSet = mockPersonResultSet(listOf("John", null)); + mappedPerson = mapper.apply(mockedResultSet); + + assertEquals(mappedPerson, new Person("John", null)); + + mockedResultSet = mockPersonResultSet(listOf("John", "Doe", "Test")); + mapper.apply(mockedResultSet); + } + @Test public void testConnectionProviderFromDataSource() throws SQLException { DataSource d = mock(DataSource.class); diff --git a/rxjava2-jdbc/src/test/java/org/davidmoten/rx/jdbc/fixtures/Fixtures.java b/rxjava2-jdbc/src/test/java/org/davidmoten/rx/jdbc/fixtures/Fixtures.java new file mode 100644 index 00000000..180753a7 --- /dev/null +++ b/rxjava2-jdbc/src/test/java/org/davidmoten/rx/jdbc/fixtures/Fixtures.java @@ -0,0 +1,37 @@ +package org.davidmoten.rx.jdbc.fixtures; + +import com.github.davidmoten.guavamini.Lists; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; +import java.util.List; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class Fixtures { + public static List listOf(String ... params) { + return Lists.newArrayList(params); + } + + public static ResultSet mockPersonResultSet(List parameterValues) throws SQLException { + ResultSet rs = mock(ResultSet.class); + ResultSetMetaData rsMeta = mock(ResultSetMetaData.class); + + when(rsMeta.getColumnCount()).thenReturn(parameterValues.size()); + + for(int i = 0; i < parameterValues.size(); i++) { + when(rsMeta.getColumnType(i)).thenReturn(Types.VARCHAR); + } + + when(rs.getMetaData()).thenReturn(rsMeta); + + for(int i = 0; i < parameterValues.size(); i++) { + when(rs.getObject(i + 1)).thenReturn(parameterValues.get(i)); + } + + return rs; + } +} diff --git a/rxjava2-jdbc/src/test/java/org/davidmoten/rx/jdbc/fixtures/Person.java b/rxjava2-jdbc/src/test/java/org/davidmoten/rx/jdbc/fixtures/Person.java new file mode 100644 index 00000000..eb34e276 --- /dev/null +++ b/rxjava2-jdbc/src/test/java/org/davidmoten/rx/jdbc/fixtures/Person.java @@ -0,0 +1,27 @@ +package org.davidmoten.rx.jdbc.fixtures; + +import java.util.Objects; + +public class Person { + public final String firstName; + public final String lastName; + + public Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Person test = (Person) o; + return Objects.equals(firstName, test.firstName) && + Objects.equals(lastName, test.lastName); + } + + @Override + public int hashCode() { + return Objects.hash(firstName, lastName); + } +}