Skip to content

Add automap support for concrete classes #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 107 additions & 4 deletions rxjava2-jdbc/src/main/java/org/davidmoten/rx/jdbc/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -719,18 +716,82 @@ static <T> ResultSetMapper<T> autoMap(Class<T> 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
if (rs != this.rs) {
this.rs = rs;
proxyService = new ProxyService<T>(rs, cls);
}

return autoMap(rs, cls, proxyService);
}
};
}

private static <T> T autoMapClass(ResultSet rs, Class<T> cls) {
try {
int n = rs.getMetaData().getColumnCount();
for (Constructor<?> c : cls.getDeclaredConstructors()) {
if (n == c.getParameterTypes().length) {
return autoMap(rs, (Constructor<T>) 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 <code>T</code> then returns an instance of type <code>T</code>.
*
* @param rs
* the result set row
* @param c
* constructor to use for instantiation
* @return automapped instance
*/
private static <T> T autoMap(ResultSet rs, Constructor<T> c) {
Class<?>[] types = c.getParameterTypes();
List<Object> list = new ArrayList<Object>();
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> T newInstance(Constructor<?> c, List<Object> 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 <code>T</code>
Expand Down Expand Up @@ -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<Object> 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() {

Expand Down
27 changes: 22 additions & 5 deletions rxjava2-jdbc/src/test/java/org/davidmoten/rx/jdbc/UtilTest.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<Person> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> listOf(String ... params) {
return Lists.newArrayList(params);
}

public static ResultSet mockPersonResultSet(List<String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}