package com.github.davidmoten.rx.jdbc; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.StringReader; import java.io.Writer; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Blob; import java.sql.Clob; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.github.davidmoten.rx.jdbc.QuerySelect.Builder; import com.github.davidmoten.rx.jdbc.exceptions.SQLRuntimeException; import rx.functions.Func1; /** * Utility methods. */ public final class Util { /** * Private constructor to prevent instantiation. */ private Util() { // prevent instantiation } /** * Logger. */ private static final Logger log = LoggerFactory.getLogger(Util.class); /** * Count the number of JDBC parameters in a sql statement. * * @param query * .sql() * @return */ static int parametersCount(Query query) { if (query.names().isEmpty()) return countQuestionMarkParameters(query.sql()); else return query.names().size(); } // Visible for testing static int countQuestionMarkParameters(String sql) { // was originally using regular expressions, but they didn't work well // for ignoring parameter-like strings inside quotes. int count = 0; int length = sql.length(); boolean inSingleQuote = false; boolean inDoubleQuote = false; for (int i = 0; i < length; i++) { char c = sql.charAt(i); if (inSingleQuote) { if (c == '\'') { inSingleQuote = false; } } else if (inDoubleQuote) { if (c == '"') { inDoubleQuote = false; } } else { if (c == '\'') { inSingleQuote = true; } else if (c == '"') { inDoubleQuote = true; } else if (c == '?') { count++; } } } return count; } /** * Cancels then closes a {@link PreparedStatement} and logs exceptions * without throwing. Does nothing if ps is null. * * @param ps */ static void closeQuietly(PreparedStatement ps) { try { boolean isClosed; try { if (ps != null) isClosed = ps.isClosed(); else isClosed = true; } catch (SQLException e) { log.debug(e.getMessage()); isClosed = true; } if (ps != null && !isClosed) { try { ps.cancel(); log.debug("cancelled {}", ps); } catch (SQLException e) { log.debug(e.getMessage()); } ps.close(); log.debug("closed {}", ps); } } catch (SQLException e) { log.debug(e.getMessage(), e); } catch (RuntimeException e) { log.debug(e.getMessage(), e); } } /** * Closes a {@link Connection} and logs exceptions without throwing. Does * nothing if connection is null. * * @param connection */ static void closeQuietly(Connection connection) { try { if (connection != null && !connection.isClosed()) { connection.close(); log.debug("closed {}", connection); } } catch (SQLException e) { log.debug(e.getMessage(), e); } catch (RuntimeException e) { log.debug(e.getMessage(), e); } } /** * Closes a {@link Connection} only if the connection is in auto commit mode * and logs exceptions without throwing. Does nothing if connection is null. * * @param connection */ static boolean closeQuietlyIfAutoCommit(Connection connection) { try { if (connection != null && !connection.isClosed() && connection.getAutoCommit()) { closeQuietly(connection); return true; } else return false; } catch (SQLException e) { throw new SQLRuntimeException(e); } } /** * Commits a {@link Connection} and logs exceptions without throwing. * * @param connection */ static void commit(Connection connection) { if (connection != null) try { connection.commit(); log.debug("committed"); } catch (SQLException e) { throw new SQLRuntimeException(e); } } /** * Rolls back a {@link Connection} and logs exceptions without throwing. * * @param connection */ static void rollback(Connection connection) { if (connection != null) try { connection.rollback(); log.debug("rolled back"); } catch (SQLException e) { throw new SQLRuntimeException(e); } } /** * Closes a {@link ResultSet} and logs exceptions without throwing. * * @param rs */ static void closeQuietly(ResultSet rs) { try { if (rs != null && !rs.isClosed()) { rs.close(); log.debug("closed {}", rs); } } catch (SQLException e) { log.debug(e.getMessage(), e); } catch (RuntimeException e) { log.debug(e.getMessage(), e); } } /** * Returns true if and only if {@link Connection} is in auto commit mode. * * @param con * @return */ static boolean isAutoCommit(Connection con) { try { return con.getAutoCommit(); } catch (SQLException e) { throw new SQLRuntimeException(e); } } /** * Returns the empty list whenever called. */ static Func1<Integer, List<Parameter>> TO_EMPTY_PARAMETER_LIST = new Func1<Integer, List<Parameter>>() { @Override public List<Parameter> call(Integer n) { return Collections.emptyList(); }; }; /** * Returns a function that converts the ResultSet column values into * parameters to the constructor (with number of parameters equals the * number of columns) of type <code>cls</code> then returns an instance of * type <code>cls</code>. See {@link Builder#autoMap(Class)}. * * @param cls * @return */ static <T> ResultSetMapper<T> autoMap(final Class<T> cls) { return new ResultSetMapper<T>() { @Override public T call(ResultSet rs) { return autoMap(rs, cls); } }; } /** * Converts the ResultSet column values into parameters to the constructor * (with number of parameters equals the number of columns) of type * <code>T</code> then returns an instance of type <code>T</code>. See See * {@link Builder#autoMap(Class)}. * * @param cls * the class of the resultant instance * @return an automapped instance */ @SuppressWarnings("unchecked") static <T> T autoMap(ResultSet rs, Class<T> cls) { try { if (cls.isInterface()) { return autoMapInterface(rs, cls); } else { 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); } } private static <T> T autoMapInterface(ResultSet rs, Class<T> cls) { return ProxyService.newInstance(rs, cls); } static interface Col { Class<?> returnType(); } static class NamedCol implements Col { final String name; private final Class<?> returnType; public NamedCol(String name, Class<?> returnType) { this.name = name; this.returnType = returnType; } @Override public Class<?> returnType() { return returnType; } @Override public String toString() { return "NamedCol [name=" + name + ", returnType=" + returnType + "]"; } } static class IndexedCol implements Col { final int index; private final Class<?> returnType; public IndexedCol(int index, Class<?> returnType) { this.index = index; this.returnType = returnType; } @Override public Class<?> returnType() { return returnType; } @Override public String toString() { return "IndexedCol [index=" + index + ", returnType=" + returnType + "]"; } } private static class ProxyService<T> implements java.lang.reflect.InvocationHandler { private final Map<String, Object> values = new HashMap<String, Object>(); ProxyService(ResultSet rs, Class<T> cls) { // load information from cache about the result set if (Database.rsCache.get() == null || Database.rsCache.get().rs != rs) Database.rsCache.set(new ResultSetCache(rs)); Map<String, Integer> colIndexes = Database.rsCache.get().colIndexes; // load information from cache about the class if (Database.autoMapCache.get() == null || Database.autoMapCache.get().cls != cls) Database.autoMapCache.set(new AutoMapCache(cls)); Map<String, Col> methodCols = Database.autoMapCache.get().methodCols; // calculate values for all the interface methods and put them in a // map for (Method m : cls.getMethods()) { String methodName = m.getName(); Col column = methodCols.get(methodName); Integer index; if (column instanceof NamedCol) { String name = ((NamedCol) column).name; index = colIndexes.get(name.toUpperCase()); if (index == null) { throw new SQLRuntimeException( "query column names do not include '" + name + "'"); } } else { IndexedCol col = ((IndexedCol) column); index = col.index; } Object value = autoMap(getObject(rs, column.returnType(), index), column.returnType()); values.put(methodName, value); } } @SuppressWarnings("unchecked") public static <T> T newInstance(ResultSet rs, Class<T> cls) { return (T) java.lang.reflect.Proxy.newProxyInstance(cls.getClassLoader(), new Class[] { cls }, new ProxyService<T>(rs, cls)); } @Override public Object invoke(Object proxy, Method m, Object[] args) throws Throwable { return values.get(m.getName()); } } static String camelCaseToUnderscore(String camelCased) { // guava has best solution for this with CaseFormat class // but don't want to add dependency just for this method final String regex = "([a-z])([A-Z]+)"; final String replacement = "$1_$2"; return camelCased.replaceAll(regex, replacement); } static String first(String[] value) { if (value == null || value.length == 0) return null; else return value[0]; } /** * 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>. See * See {@link Builder#autoMap(Class)}. * * @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); } } static <T> void setSqlFromQueryAnnotation(Class<T> cls, QueryBuilder builder) { if (builder.sql() == null) { com.github.davidmoten.rx.jdbc.annotations.Query query = cls .getAnnotation(com.github.davidmoten.rx.jdbc.annotations.Query.class); if (query != null && query.value() != null) { String sql = query.value(); builder.setSql(sql); } else throw new RuntimeException("Class " + cls + " must be annotated with @Query(sql) or sql must be specified to the builder.select() call"); } } /** * 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(); } /** * * @param c * constructor to use * @param parameters * constructor parameters * @return */ @SuppressWarnings("unchecked") private static <T> T newInstance(Constructor<?> c, List<Object> parameters) { try { return (T) c.newInstance(parameters.toArray()); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (IllegalArgumentException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } } /** * Converts from java.sql Types to common java types like java.util.Date and * numeric types. See {@link Builder#autoMap(Class)}. * * @param o * @param cls * @return */ public static Object autoMap(Object o, Class<?> cls) { if (o == null) return o; else if (cls.isAssignableFrom(o.getClass())) { return o; } else { if (o instanceof java.sql.Date) { java.sql.Date d = (java.sql.Date) o; if (cls.isAssignableFrom(Long.class)) return d.getTime(); else if (cls.isAssignableFrom(BigInteger.class)) return BigInteger.valueOf(d.getTime()); else return o; } else if (o instanceof java.sql.Timestamp) { Timestamp t = (java.sql.Timestamp) o; if (cls.isAssignableFrom(Long.class)) return t.getTime(); else if (cls.isAssignableFrom(BigInteger.class)) return BigInteger.valueOf(t.getTime()); else return o; } else if (o instanceof java.sql.Time) { Time t = (java.sql.Time) o; if (cls.isAssignableFrom(Long.class)) return t.getTime(); else if (cls.isAssignableFrom(BigInteger.class)) return BigInteger.valueOf(t.getTime()); else return o; } else if (o instanceof Blob && cls.isAssignableFrom(byte[].class)) { return toBytes((Blob) o); } else if (o instanceof Clob && cls.isAssignableFrom(String.class)) { return toString((Clob) o); } else if (o instanceof BigInteger && cls.isAssignableFrom(Long.class)) { return ((BigInteger) o).longValue(); } else if (o instanceof BigInteger && cls.isAssignableFrom(Integer.class)) { return ((BigInteger) o).intValue(); } else if (o instanceof BigInteger && cls.isAssignableFrom(Double.class)) { return ((BigInteger) o).doubleValue(); } else if (o instanceof BigInteger && cls.isAssignableFrom(Float.class)) { return ((BigInteger) o).floatValue(); } else if (o instanceof BigInteger && cls.isAssignableFrom(Short.class)) { return ((BigInteger) o).shortValue(); } else if (o instanceof BigInteger && cls.isAssignableFrom(BigDecimal.class)) { return new BigDecimal((BigInteger) o); } else if (o instanceof BigDecimal && cls.isAssignableFrom(Double.class)) { return ((BigDecimal) o).doubleValue(); } else if (o instanceof BigDecimal && cls.isAssignableFrom(Integer.class)) { return ((BigDecimal) o).toBigInteger().intValue(); } else if (o instanceof BigDecimal && cls.isAssignableFrom(Float.class)) { return ((BigDecimal) o).floatValue(); } else if (o instanceof BigDecimal && cls.isAssignableFrom(Short.class)) { return ((BigDecimal) o).toBigInteger().shortValue(); } else if (o instanceof BigDecimal && cls.isAssignableFrom(Long.class)) { return ((BigDecimal) o).toBigInteger().longValue(); } else if (o instanceof BigDecimal && cls.isAssignableFrom(BigInteger.class)) { return ((BigDecimal) o).toBigInteger(); } else if ((o instanceof Short || o instanceof Integer || o instanceof Long) && cls.isAssignableFrom(BigInteger.class)) { return new BigInteger(o.toString()); } else if (o instanceof Number && cls.isAssignableFrom(BigDecimal.class)) { return new BigDecimal(o.toString()); } else if (o instanceof Number && cls.isAssignableFrom(Short.class)) return ((Number) o).shortValue(); else if (o instanceof Number && cls.isAssignableFrom(Integer.class)) return ((Number) o).intValue(); else if (o instanceof Number && cls.isAssignableFrom(Integer.class)) return ((Number) o).intValue(); else if (o instanceof Number && cls.isAssignableFrom(Long.class)) return ((Number) o).longValue(); else if (o instanceof Number && cls.isAssignableFrom(Float.class)) return ((Number) o).floatValue(); else if (o instanceof Number && cls.isAssignableFrom(Double.class)) return ((Number) o).doubleValue(); else return o; } } public static <T> Object mapObject(final ResultSet rs, Class<T> cls, int i) { return autoMap(getObject(rs, cls, i), cls); } private static <T> Object getObject(final ResultSet rs, Class<T> cls, int i) { try { if (rs.getObject(i) == null) { return null; } final int type = rs.getMetaData().getColumnType(i); // TODO java.util.Calendar support // TODO XMLGregorian Calendar support if (type == Types.DATE) return rs.getDate(i, Calendar.getInstance()); else if (type == Types.TIME) return rs.getTime(i, Calendar.getInstance()); else if (type == Types.TIMESTAMP) return rs.getTimestamp(i, Calendar.getInstance()); else if (type == Types.CLOB && cls.equals(String.class)) { return toString(rs.getClob(i)); } else if (type == Types.CLOB && Reader.class.isAssignableFrom(cls)) { Clob c = rs.getClob(i); Reader r = c.getCharacterStream(); return createFreeOnCloseReader(c, r); } else if (type == Types.BLOB && cls.equals(byte[].class)) { return toBytes(rs.getBlob(i)); } else if (type == Types.BLOB && InputStream.class.isAssignableFrom(cls)) { final Blob b = rs.getBlob(i); final InputStream is = rs.getBlob(i).getBinaryStream(); return createFreeOnCloseInputStream(b, is); } else return rs.getObject(i); } catch (SQLException e) { throw new SQLRuntimeException(e); } } /** * Returns the bytes of a {@link Blob} and frees the blob resource. * * @param b * blob * @return */ private static byte[] toBytes(Blob b) { try { InputStream is = b.getBinaryStream(); byte[] result = IOUtils.toByteArray(is); is.close(); b.free(); return result; } catch (IOException e) { throw new RuntimeException(e); } catch (SQLException e) { throw new SQLRuntimeException(e); } } /** * Returns the String of a {@link Clob} and frees the clob resource. * * @param c * @return */ private static String toString(Clob c) { try { Reader reader = c.getCharacterStream(); String result = IOUtils.toString(reader); reader.close(); c.free(); return result; } catch (IOException e) { throw new RuntimeException(e); } catch (SQLException e) { throw new SQLRuntimeException(e); } } /** * Automatically frees the blob (<code>blob.free()</code>) once the blob * {@link InputStream} is closed. * * @param blob * @param is * @return */ private static InputStream createFreeOnCloseInputStream(final Blob blob, final InputStream is) { return new InputStream() { @Override public int read() throws IOException { return is.read(); } @Override public void close() throws IOException { try { is.close(); } finally { try { blob.free(); } catch (SQLException e) { log.debug(e.getMessage()); } } } }; } /** * Automatically frees the clob (<code>Clob.free()</code>) once the clob * Reader is closed. * * @param clob * @param reader * @return */ private static Reader createFreeOnCloseReader(final Clob clob, final Reader reader) { return new Reader() { @Override public void close() throws IOException { try { reader.close(); } finally { try { clob.free(); } catch (SQLException e) { log.debug(e.getMessage()); } } } @Override public int read(char[] cbuf, int off, int len) throws IOException { return reader.read(cbuf, off, len); } }; } /** * Sets parameters for the {@link PreparedStatement}. * * @param ps * @param params * @throws SQLException */ static void setParameters(PreparedStatement ps, List<Parameter> params, boolean namesAllowed) throws SQLException { for (int i = 1; i <= params.size(); i++) { if (params.get(i - 1).hasName() && !namesAllowed) throw new SQLException("named parameter found but sql does not contain names"); Object o = params.get(i - 1).value(); try { if (o == null) ps.setObject(i, null); else if (o == Database.NULL_CLOB) ps.setNull(i, Types.CLOB); else if (o == Database.NULL_BLOB) ps.setNull(i, Types.BLOB); else { Class<?> cls = o.getClass(); if (Clob.class.isAssignableFrom(cls)) { setClob(ps, i, o, cls); } else if (Blob.class.isAssignableFrom(cls)) { setBlob(ps, i, o, cls); } else if (Calendar.class.isAssignableFrom(cls)) { Calendar cal = (Calendar) o; Timestamp t = new java.sql.Timestamp(cal.getTimeInMillis()); ps.setTimestamp(i, t, cal); } else if (Time.class.isAssignableFrom(cls)) { Calendar cal = Calendar.getInstance(); ps.setTime(i, (Time) o, cal); } else if (Timestamp.class.isAssignableFrom(cls)) { Calendar cal = Calendar.getInstance(); ps.setTimestamp(i, (Timestamp) o, cal); } else if (java.sql.Date.class.isAssignableFrom(cls)) { Calendar cal = Calendar.getInstance(); ps.setDate(i, (java.sql.Date) o, cal); } else if (java.util.Date.class.isAssignableFrom(cls)) { Calendar cal = Calendar.getInstance(); java.util.Date date = (java.util.Date) o; ps.setTimestamp(i, new java.sql.Timestamp(date.getTime()), cal); } else ps.setObject(i, o); } } catch (SQLException e) { log.debug("{} when setting ps.setObject({},{})", e.getMessage(), i, o); throw e; } } } /** * Sets a blob parameter for the prepared statement. * * @param ps * @param i * @param o * @param cls * @throws SQLException */ private static void setBlob(PreparedStatement ps, int i, Object o, Class<?> cls) throws SQLException { final InputStream is; if (o instanceof byte[]) { is = new ByteArrayInputStream((byte[]) o); } else if (o instanceof InputStream) is = (InputStream) o; else throw new RuntimeException( "cannot insert parameter of type " + cls + " into blob column " + i); Blob c = ps.getConnection().createBlob(); OutputStream os = c.setBinaryStream(1); copy(is, os); ps.setBlob(i, c); } /** * Sets the clob parameter for the prepared statement. * * @param ps * @param i * @param o * @param cls * @throws SQLException */ private static void setClob(PreparedStatement ps, int i, Object o, Class<?> cls) throws SQLException { final Reader r; if (o instanceof String) r = new StringReader((String) o); else if (o instanceof Reader) r = (Reader) o; else throw new RuntimeException( "cannot insert parameter of type " + cls + " into clob column " + i); Clob c = ps.getConnection().createClob(); Writer w = c.setCharacterStream(1); copy(r, w); ps.setClob(i, c); } /** * Copies a {@link Reader} to a {@link Writer}. * * @param input * @param output * @return */ private static int copy(Reader input, Writer output) { try { return IOUtils.copy(input, output); } catch (IOException e) { throw new RuntimeException(e); } } /** * Copies an {@link InputStream} to an {@link OutputStream}. * * @param input * @param output * @return */ private static int copy(InputStream input, OutputStream output) { try { return IOUtils.copy(input, output); } catch (IOException e) { throw new RuntimeException(e); } } /** * Returns a function that reads a {@link Reader} into a String. */ public static final Func1<Reader, String> READER_TO_STRING = new Func1<Reader, String>() { @Override public String call(Reader r) { try { return IOUtils.toString(r); } catch (IOException e) { throw new RuntimeException(e); } } }; // Lazy singleton private static final class ResultSetMapperToOne { static final ResultSetMapper<Integer> INSTANCE = new ResultSetMapper<Integer>() { @Override public Integer call(ResultSet rs) { return 1; } }; } static ResultSetMapper<Integer> toOne() { return ResultSetMapperToOne.INSTANCE; } public static void setNamedParameters(PreparedStatement ps, List<Parameter> parameters, List<String> names) throws SQLException { Map<String, Parameter> map = new HashMap<String, Parameter>(); for (Parameter p : parameters) { if (p.hasName()) { map.put(p.name(), p); } else { throw new SQLException( "named parameters were expected but this parameter did not have a name: " + p); } } List<Parameter> list = new ArrayList<Parameter>(); for (String name : names) { if (!map.containsKey(name)) throw new SQLException("named parameter is missing for '" + name + "'"); Parameter p = map.get(name); list.add(p); } Util.setParameters(ps, list, true); } static void setParameters(PreparedStatement ps, List<Parameter> parameters, List<String> names) throws SQLException { if (names.isEmpty()) { Util.setParameters(ps, parameters, false); } else { Util.setNamedParameters(ps, parameters, names); } } }