View Javadoc
1   package org.davidmoten.rxjava3.jdbc;
2   
3   import java.io.IOException;
4   import java.io.InputStream;
5   import java.io.Reader;
6   import java.lang.invoke.MethodHandles;
7   import java.lang.invoke.MethodType;
8   import java.lang.reflect.Constructor;
9   import java.lang.reflect.Method;
10  import java.lang.reflect.Modifier;
11  import java.lang.reflect.Proxy;
12  import java.math.BigDecimal;
13  import java.math.BigInteger;
14  import java.sql.Blob;
15  import java.sql.CallableStatement;
16  import java.sql.Clob;
17  import java.sql.Connection;
18  import java.sql.DriverManager;
19  import java.sql.PreparedStatement;
20  import java.sql.ResultSet;
21  import java.sql.ResultSetMetaData;
22  import java.sql.SQLException;
23  import java.sql.Statement;
24  import java.sql.Time;
25  import java.sql.Timestamp;
26  import java.sql.Types;
27  import java.time.Instant;
28  import java.time.ZonedDateTime;
29  import java.util.ArrayList;
30  import java.util.Calendar;
31  import java.util.Collection;
32  import java.util.HashMap;
33  import java.util.List;
34  import java.util.Locale;
35  import java.util.Map;
36  import java.util.Map.Entry;
37  import java.util.Properties;
38  import java.util.TreeMap;
39  import java.util.concurrent.atomic.AtomicReference;
40  import java.util.stream.Collectors;
41  
42  import javax.annotation.RegEx;
43  import javax.sql.DataSource;
44  
45  import org.apache.commons.io.IOUtils;
46  import org.davidmoten.rxjava3.jdbc.annotations.Column;
47  import org.davidmoten.rxjava3.jdbc.annotations.Index;
48  import org.davidmoten.rxjava3.jdbc.callable.internal.OutParameterPlaceholder;
49  import org.davidmoten.rxjava3.jdbc.callable.internal.ParameterPlaceholder;
50  import org.davidmoten.rxjava3.jdbc.exceptions.AnnotationsNotFoundException;
51  import org.davidmoten.rxjava3.jdbc.exceptions.AutomappedInterfaceInaccessibleException;
52  import org.davidmoten.rxjava3.jdbc.exceptions.ColumnIndexOutOfRangeException;
53  import org.davidmoten.rxjava3.jdbc.exceptions.ColumnNotFoundException;
54  import org.davidmoten.rxjava3.jdbc.exceptions.MoreColumnsRequestedThanExistException;
55  import org.davidmoten.rxjava3.jdbc.exceptions.NamedParameterFoundButSqlDoesNotHaveNamesException;
56  import org.davidmoten.rxjava3.jdbc.exceptions.NamedParameterMissingException;
57  import org.davidmoten.rxjava3.jdbc.exceptions.ParameterMissingNameException;
58  import org.davidmoten.rxjava3.jdbc.exceptions.SQLRuntimeException;
59  import org.slf4j.Logger;
60  import org.slf4j.LoggerFactory;
61  
62  import com.github.davidmoten.guavamini.annotations.VisibleForTesting;
63  
64  public enum Util {
65      ;
66  
67      private static final Logger log = LoggerFactory.getLogger(Util.class);
68      public static final int QUERY_TIMEOUT_NOT_SET = -1;
69  
70      /**
71       * Sets parameters for the {@link PreparedStatement}.
72       * 
73       * @param ps
74       * @param params
75       * @throws SQLException
76       */
77      @VisibleForTesting
78      static void setParameters(PreparedStatement ps, List<Parameter> params, boolean namesAllowed) throws SQLException {
79          int j = 1;
80          for (int i = 0; i < params.size(); i++) {
81              Parameter p = params.get(i);
82              if (p.hasName() && !namesAllowed) {
83                  throw new NamedParameterFoundButSqlDoesNotHaveNamesException(
84                          "named parameter found but sql does not contain names ps=" + ps);
85              }
86              Object v = p.value();
87              if (p.isCollection()) {
88                  for (Object o : (Collection<?>) v) {
89                      setParameter(ps, j, o);
90                      j++;
91                  }
92              } else {
93                  setParameter(ps, j, v);
94                  j++;
95              }
96          }
97      }
98  
99      static void setParameter(PreparedStatement ps, int i, Object o) throws SQLException {
100         log.debug("setting parameter {} to {}", i, o);
101         try {
102             if (o == null)
103                 ps.setObject(i, null);
104             else if (o == Database.NULL_CLOB)
105                 ps.setNull(i, Types.CLOB);
106             else if (o == Database.NULL_BLOB)
107                 ps.setNull(i, Types.BLOB);
108             else {
109                 Class<?> cls = o.getClass();
110                 if (Clob.class.isAssignableFrom(cls)) {
111                     setClob(ps, i, o, cls);
112                 } else if (Blob.class.isAssignableFrom(cls)) {
113                     setBlob(ps, i, o, cls);
114                 } else if (Calendar.class.isAssignableFrom(cls)) {
115                     Calendar cal = (Calendar) o;
116                     Timestamp t = new Timestamp(cal.getTimeInMillis());
117                     ps.setTimestamp(i, t);
118                 } else if (Time.class.isAssignableFrom(cls)) {
119                     Calendar cal = Calendar.getInstance();
120                     ps.setTime(i, (Time) o, cal);
121                 } else if (Timestamp.class.isAssignableFrom(cls)) {
122                     Calendar cal = Calendar.getInstance();
123                     ps.setTimestamp(i, (Timestamp) o, cal);
124                 } else if (java.sql.Date.class.isAssignableFrom(cls)) {
125                     ps.setDate(i, (java.sql.Date) o, Calendar.getInstance());
126                 } else if (java.util.Date.class.isAssignableFrom(cls)) {
127                     Calendar cal = Calendar.getInstance();
128                     java.util.Date date = (java.util.Date) o;
129                     ps.setTimestamp(i, new Timestamp(date.getTime()), cal);
130                 } else if (Instant.class.isAssignableFrom(cls)) {
131                     Calendar cal = Calendar.getInstance();
132                     Instant instant = (Instant) o;
133                     ps.setTimestamp(i, new Timestamp(instant.toEpochMilli()), cal);
134                 } else if (ZonedDateTime.class.isAssignableFrom(cls)) {
135                     Calendar cal = Calendar.getInstance();
136                     ZonedDateTime d = (ZonedDateTime) o;
137                     ps.setTimestamp(i, new Timestamp(d.toInstant().toEpochMilli()), cal);
138                 } else
139                     ps.setObject(i, o);
140             }
141         } catch (SQLException e) {
142             log.debug("{} when setting ps.setObject({},{})", e.getMessage(), i, o);
143             throw e;
144         }
145     }
146 
147     /**
148      * Sets a blob parameter for the prepared statement.
149      * 
150      * @param ps
151      * @param i
152      * @param o
153      * @param cls
154      * @throws SQLException
155      */
156     private static void setBlob(PreparedStatement ps, int i, Object o, Class<?> cls) throws SQLException {
157         // if (o instanceof Blob) {
158         ps.setBlob(i, (Blob) o);
159         // } else {
160         // final InputStream is;
161         // if (o instanceof byte[]) {
162         // is = new ByteArrayInputStream((byte[]) o);
163         // } else if (o instanceof InputStream)
164         // is = (InputStream) o;
165         // else
166         // throw new RuntimeException("cannot insert parameter of type " + cls + " into
167         // blob column " + i);
168         // Blob c = ps.getConnection().createBlob();
169         // OutputStream os = c.setBinaryStream(1);
170         // copy(is, os);
171         // ps.setBlob(i, c);
172         // }
173     }
174 
175     /**
176      * Sets the clob parameter for the prepared statement.
177      * 
178      * @param ps
179      * @param i
180      * @param o
181      * @param cls
182      * @throws SQLException
183      */
184     private static void setClob(PreparedStatement ps, int i, Object o, Class<?> cls) throws SQLException {
185         // if (o instanceof Clob) {
186         ps.setClob(i, (Clob) o);
187         // } else {
188         // final Reader r;
189         // if (o instanceof String)
190         // r = new StringReader((String) o);
191         // else if (o instanceof Reader)
192         // r = (Reader) o;
193         // else
194         // throw new RuntimeException("cannot insert parameter of type " + cls + " into
195         // clob column " + i);
196         // Clob c = ps.getConnection().createClob();
197         // Writer w = c.setCharacterStream(1);
198         // copy(r, w);
199         // ps.setClob(i, c);
200         // }
201     }
202 
203     static void setNamedParameters(PreparedStatement ps, List<Parameter> parameters, List<String> names)
204             throws SQLException {
205         Map<String, Parameter> map = createMap(parameters);
206         List<Parameter> list = new ArrayList<Parameter>();
207         for (String name : names) {
208             if (!map.containsKey(name))
209                 throw new NamedParameterMissingException("named parameter is missing for '" + name + "'");
210             Parameter p = map.get(name);
211             list.add(p);
212         }
213         Util.setParameters(ps, list, true);
214     }
215 
216     @VisibleForTesting
217     static Map<String, Parameter> createMap(List<Parameter> parameters) {
218         Map<String, Parameter> map = new HashMap<String, Parameter>();
219         for (Parameter p : parameters) {
220             if (p.hasName()) {
221                 map.put(p.name(), p);
222             } else {
223                 throw new ParameterMissingNameException(
224                         "named parameters were expected but this parameter did not have a name: " + p);
225             }
226         }
227         return map;
228     }
229 
230     static PreparedStatement convertAndSetParameters(PreparedStatement ps, List<Object> parameters, List<String> names)
231             throws SQLException {
232         return setParameters(ps, toParameters(parameters), names);
233     }
234 
235     static PreparedStatement setParameters(PreparedStatement ps, List<Parameter> parameters, List<String> names)
236             throws SQLException {
237         if (names.isEmpty()) {
238             Util.setParameters(ps, parameters, false);
239         } else {
240             Util.setNamedParameters(ps, parameters, names);
241         }
242         return ps;
243     }
244 
245     static List<Parameter> toParameters(List<Object> parameters) {
246         return parameters.stream().map(o -> {
247             if (o instanceof Parameter) {
248                 return (Parameter) o;
249             } else {
250                 return new Parameter(o);
251             }
252         }).collect(Collectors.toList());
253     }
254 
255     static void incrementCounter(Connection connection) {
256         if (connection instanceof TransactedConnection) {
257             TransactedConnection c = (TransactedConnection) connection;
258             c.incrementCounter();
259         }
260     }
261 
262     public static void closeSilently(AutoCloseable c) {
263         if (c != null) {
264             try {
265                 log.debug("closing {}", c);
266                 c.close();
267             } catch (Exception e) {
268                 log.debug("ignored exception {}, {}, {}", e.getMessage(), e.getClass(), e);
269             }
270         }
271     }
272 
273     static void closePreparedStatementAndConnection(PreparedStatement ps) {
274         Connection con = null;
275         try {
276             con = ps.getConnection();
277         } catch (SQLException e) {
278             log.warn(e.getMessage(), e);
279         }
280         closeSilently(ps);
281         closeSilently(con);
282     }
283 
284     static void closePreparedStatementAndConnection(NamedPreparedStatement ps) {
285         closePreparedStatementAndConnection(ps.ps);
286     }
287 
288     static void closeCallableStatementAndConnection(NamedCallableStatement stmt) {
289         closePreparedStatementAndConnection(stmt.stmt);
290     }
291 
292     static NamedPreparedStatement prepare(Connection con, String sql, int queryTimeoutSec) throws SQLException {
293         return prepare(con, 0, sql, queryTimeoutSec);
294     }
295 
296     static NamedPreparedStatement prepare(Connection con, int fetchSize, String sql, int queryTimeoutSec) throws SQLException {
297         // TODO can we parse SqlInfo through because already calculated by
298         // builder?
299         SqlInfo info = SqlInfo.parse(sql);
300         log.debug("preparing statement: {}", sql);
301         return prepare(con, fetchSize, info, queryTimeoutSec);
302     }
303 
304     static PreparedStatement prepare(Connection connection, int fetchSize, String sql, List<Parameter> parameters, int queryTimeoutSec)
305             throws SQLException {
306         // should only get here when parameters contains a collection
307         SqlInfo info = SqlInfo.parse(sql, parameters);
308         log.debug("preparing statement: {}", info.sql());
309         return createPreparedStatement(connection, fetchSize, info, queryTimeoutSec);
310     }
311 
312     private static NamedPreparedStatement prepare(Connection con, int fetchSize, SqlInfo info, int queryTimeoutSec) throws SQLException {
313         PreparedStatement ps = createPreparedStatement(con, fetchSize, info, queryTimeoutSec);
314         return new NamedPreparedStatement(ps, info.names());
315     }
316 
317     private static PreparedStatement createPreparedStatement(Connection con, int fetchSize, SqlInfo info, int queryTimeoutSec)
318             throws SQLException {
319         PreparedStatement ps = null;
320         try {
321             ps = con.prepareStatement(info.sql(), ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
322             if (fetchSize > 0) {
323                 ps.setFetchSize(fetchSize);
324             }
325 
326             if (queryTimeoutSec != QUERY_TIMEOUT_NOT_SET) {
327                 ps.setQueryTimeout(queryTimeoutSec);
328             }
329         } catch (RuntimeException | SQLException e) {
330             if (ps != null) {
331                 ps.close();
332             }
333             throw e;
334         }
335         return ps;
336     }
337 
338     static NamedCallableStatement prepareCall(Connection con, String sql,
339             List<ParameterPlaceholder> parameterPlaceholders) throws SQLException {
340         return prepareCall(con, 0, sql, parameterPlaceholders);
341     }
342 
343     // TODO is fetchSize required for callablestatement
344     static NamedCallableStatement prepareCall(Connection con, int fetchSize, String sql,
345             List<ParameterPlaceholder> parameterPlaceholders) throws SQLException {
346         // TODO can we parse SqlInfo through because already calculated by
347         // builder?
348         SqlInfo s = SqlInfo.parse(sql);
349         log.debug("preparing statement: {}", sql);
350         CallableStatement ps = null;
351         try {
352             ps = con.prepareCall(s.sql(), ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
353             for (int i = 0; i < parameterPlaceholders.size(); i++) {
354                 ParameterPlaceholder p = parameterPlaceholders.get(i);
355                 if (p instanceof OutParameterPlaceholder) {
356                     ps.registerOutParameter(i + 1, ((OutParameterPlaceholder) p).type().value());
357                 }
358             }
359             if (fetchSize > 0) {
360                 ps.setFetchSize(fetchSize);
361             }
362             return new NamedCallableStatement(ps, s.names());
363         } catch (RuntimeException | SQLException e) {
364             if (ps != null) {
365                 ps.close();
366             }
367             throw e;
368         }
369     }
370 
371     static NamedPreparedStatement prepareReturnGeneratedKeys(Connection con, String sql) throws SQLException {
372         SqlInfo s = SqlInfo.parse(sql);
373         return new NamedPreparedStatement(con.prepareStatement(s.sql(), Statement.RETURN_GENERATED_KEYS), s.names());
374     }
375 
376     @VisibleForTesting
377     static int countQuestionMarkParameters(String sql) {
378         // was originally using regular expressions, but they didn't work well
379         // for ignoring parameter-like strings inside quotes.
380         int count = 0;
381         int length = sql.length();
382         boolean inSingleQuote = false;
383         boolean inDoubleQuote = false;
384         for (int i = 0; i < length; i++) {
385             char c = sql.charAt(i);
386             if (inSingleQuote) {
387                 if (c == '\'') {
388                     inSingleQuote = false;
389                 }
390             } else if (inDoubleQuote) {
391                 if (c == '"') {
392                     inDoubleQuote = false;
393                 }
394             } else {
395                 if (c == '\'') {
396                     inSingleQuote = true;
397                 } else if (c == '"') {
398                     inDoubleQuote = true;
399                 } else if (c == '?') {
400                     count++;
401                 }
402             }
403         }
404         return count;
405     }
406 
407     public static void commit(PreparedStatement ps) throws SQLException {
408         Connection c = ps.getConnection();
409         if (!c.getAutoCommit()) {
410             c.commit();
411         }
412     }
413 
414     public static void rollback(PreparedStatement ps) throws SQLException {
415         Connection c = ps.getConnection();
416         if (!c.getAutoCommit()) {
417             c.rollback();
418         }
419     }
420 
421     // auto-mapping
422 
423     /**
424      * Converts from java.sql Types to common java types like java.util.Date and
425      * numeric types.
426      * 
427      * @param o
428      *            object to map to new object of type {@code cls}
429      * @param cls
430      *            object return type
431      * @return object of type {@code cls}
432      */
433     public static Object autoMap(Object o, Class<?> cls) {
434         if (o == null)
435             return o;
436         else if (cls.isAssignableFrom(o.getClass())) {
437             return o;
438         } else {
439             if (o instanceof java.sql.Date) {
440                 java.sql.Date d = (java.sql.Date) o;
441                 if (cls.isAssignableFrom(Long.class))
442                     return d.getTime();
443                 else if (cls.isAssignableFrom(BigInteger.class))
444                     return BigInteger.valueOf(d.getTime());
445                 else if (cls.isAssignableFrom(Instant.class))
446                     return Instant.ofEpochMilli(d.getTime());
447                 else
448                     return o;
449             } else if (o instanceof Timestamp) {
450                 Timestamp t = (Timestamp) o;
451                 if (cls.isAssignableFrom(Long.class))
452                     return t.getTime();
453                 else if (cls.isAssignableFrom(BigInteger.class))
454                     return BigInteger.valueOf(t.getTime());
455                 else if (cls.isAssignableFrom(Instant.class))
456                     return t.toInstant();
457                 else
458                     return o;
459             } else if (o instanceof Time) {
460                 Time t = (Time) o;
461                 if (cls.isAssignableFrom(Long.class))
462                     return t.getTime();
463                 else if (cls.isAssignableFrom(BigInteger.class))
464                     return BigInteger.valueOf(t.getTime());
465                 else if (cls.isAssignableFrom(Instant.class))
466                     return t.toInstant();
467                 else
468                     return o;
469             } else if (o instanceof Blob && cls.isAssignableFrom(byte[].class)) {
470                 return toBytes((Blob) o);
471             } else if (o instanceof Clob && cls.isAssignableFrom(String.class)) {
472                 return toString((Clob) o);
473             } else if (o instanceof BigInteger && cls.isAssignableFrom(Long.class)) {
474                 return ((BigInteger) o).longValue();
475             } else if (o instanceof BigInteger && cls.isAssignableFrom(Integer.class)) {
476                 return ((BigInteger) o).intValue();
477             } else if (o instanceof BigInteger && cls.isAssignableFrom(Double.class)) {
478                 return ((BigInteger) o).doubleValue();
479             } else if (o instanceof BigInteger && cls.isAssignableFrom(Float.class)) {
480                 return ((BigInteger) o).floatValue();
481             } else if (o instanceof BigInteger && cls.isAssignableFrom(Short.class)) {
482                 return ((BigInteger) o).shortValue();
483             } else if (o instanceof BigInteger && cls.isAssignableFrom(BigDecimal.class)) {
484                 return new BigDecimal((BigInteger) o);
485             } else if (o instanceof BigDecimal && cls.isAssignableFrom(Double.class)) {
486                 return ((BigDecimal) o).doubleValue();
487             } else if (o instanceof BigDecimal && cls.isAssignableFrom(Integer.class)) {
488                 return ((BigDecimal) o).toBigInteger().intValue();
489             } else if (o instanceof BigDecimal && cls.isAssignableFrom(Float.class)) {
490                 return ((BigDecimal) o).floatValue();
491             } else if (o instanceof BigDecimal && cls.isAssignableFrom(Short.class)) {
492                 return ((BigDecimal) o).toBigInteger().shortValue();
493             } else if (o instanceof BigDecimal && cls.isAssignableFrom(Long.class)) {
494                 return ((BigDecimal) o).toBigInteger().longValue();
495             } else if (o instanceof BigDecimal && cls.isAssignableFrom(BigInteger.class)) {
496                 return ((BigDecimal) o).toBigInteger();
497             } else if ((o instanceof Short || o instanceof Integer || o instanceof Long)
498                     && cls.isAssignableFrom(BigInteger.class)) {
499                 return new BigInteger(o.toString());
500             } else if (o instanceof Number && cls.isAssignableFrom(BigDecimal.class)) {
501                 return new BigDecimal(o.toString());
502             } else if (o instanceof Number && cls.isAssignableFrom(Short.class))
503                 return ((Number) o).shortValue();
504             else if (o instanceof Number && cls.isAssignableFrom(Integer.class))
505                 return ((Number) o).intValue();
506             else if (o instanceof Number && cls.isAssignableFrom(Integer.class))
507                 return ((Number) o).intValue();
508             else if (o instanceof Number && cls.isAssignableFrom(Long.class))
509                 return ((Number) o).longValue();
510             else if (o instanceof Number && cls.isAssignableFrom(Float.class))
511                 return ((Number) o).floatValue();
512             else if (o instanceof Number && cls.isAssignableFrom(Double.class))
513                 return ((Number) o).doubleValue();
514             else
515                 return o;
516         }
517     }
518 
519     @SuppressWarnings("unchecked")
520     public static <T> T mapObject(final ResultSet rs, Class<T> cls, int i) {
521         return (T) autoMap(getObject(rs, cls, i), cls);
522     }
523 
524     @SuppressWarnings("unchecked")
525     public static <T> T mapObject(final CallableStatement cs, Class<T> cls, int i, Type type) {
526         return (T) autoMap(getObject(cs, cls, i, type), cls);
527     }
528 
529     private static <T> Object getObject(final ResultSet rs, Class<T> cls, int i) {
530         try {
531             int colCount = rs.getMetaData().getColumnCount();
532             if (i > colCount) {
533                 throw new MoreColumnsRequestedThanExistException(
534                         "only " + colCount + " columns exist in ResultSet and column " + i + " was requested");
535             }
536             if (rs.getObject(i) == null) {
537                 return null;
538             }
539             final int type = rs.getMetaData().getColumnType(i);
540             // TODO java.util.Calendar support
541             // TODO XMLGregorian Calendar support
542             if (type == Types.DATE)
543                 return rs.getDate(i, Calendar.getInstance());
544             else if (type == Types.TIME)
545                 return rs.getTime(i, Calendar.getInstance());
546             else if (type == Types.TIMESTAMP)
547                 return rs.getTimestamp(i, Calendar.getInstance());
548             else if (type == Types.CLOB && cls.equals(String.class)) {
549                 return toString(rs.getClob(i));
550             } else if (type == Types.CLOB && Reader.class.isAssignableFrom(cls)) {
551                 Clob c = rs.getClob(i);
552                 Reader r = c.getCharacterStream();
553                 return createFreeOnCloseReader(c, r);
554             } else if (type == Types.BLOB && cls.equals(byte[].class)) {
555                 return toBytes(rs.getBlob(i));
556             } else if (type == Types.BLOB && InputStream.class.isAssignableFrom(cls)) {
557                 final Blob b = rs.getBlob(i);
558                 final InputStream is = rs.getBlob(i).getBinaryStream();
559                 return createFreeOnCloseInputStream(b, is);
560             } else
561                 return rs.getObject(i);
562         } catch (SQLException e) {
563             throw new SQLRuntimeException(e);
564         }
565     }
566 
567     private static <T> Object getObject(final CallableStatement cs, Class<T> cls, int i, Type typ) {
568         try {
569             if (cs.getObject(i) == null) {
570                 return null;
571             }
572             final int type = typ.value();
573             // TODO java.util.Calendar support
574             // TODO XMLGregorian Calendar support
575             if (type == Types.DATE)
576                 return cs.getDate(i, Calendar.getInstance());
577             else if (type == Types.TIME)
578                 return cs.getTime(i, Calendar.getInstance());
579             else if (type == Types.TIMESTAMP)
580                 return cs.getTimestamp(i, Calendar.getInstance());
581             else if (type == Types.CLOB && cls.equals(String.class)) {
582                 return toString(cs.getClob(i));
583             } else if (type == Types.CLOB && Reader.class.isAssignableFrom(cls)) {
584                 Clob c = cs.getClob(i);
585                 Reader r = c.getCharacterStream();
586                 return createFreeOnCloseReader(c, r);
587             } else if (type == Types.BLOB && cls.equals(byte[].class)) {
588                 return toBytes(cs.getBlob(i));
589             } else if (type == Types.BLOB && InputStream.class.isAssignableFrom(cls)) {
590                 final Blob b = cs.getBlob(i);
591                 final InputStream is = cs.getBlob(i).getBinaryStream();
592                 return createFreeOnCloseInputStream(b, is);
593             } else
594                 return cs.getObject(i);
595         } catch (SQLException e) {
596             throw new SQLRuntimeException(e);
597         }
598     }
599 
600     /**
601      * Returns the bytes of a {@link Blob} and frees the blob resource.
602      * 
603      * @param b
604      *            blob
605      * @return
606      */
607     @VisibleForTesting
608     static byte[] toBytes(Blob b) {
609         try {
610             InputStream is = b.getBinaryStream();
611             byte[] result = IOUtils.toByteArray(is);
612             is.close();
613             b.free();
614             return result;
615         } catch (IOException e) {
616             throw new RuntimeException(e);
617         } catch (SQLException e) {
618             throw new SQLRuntimeException(e);
619         }
620 
621     }
622 
623     /**
624      * Returns the String of a {@link Clob} and frees the clob resource.
625      * 
626      * @param c
627      * @return
628      */
629     @VisibleForTesting
630     static String toString(Clob c) {
631         try {
632             Reader reader = c.getCharacterStream();
633             String result = IOUtils.toString(reader);
634             reader.close();
635             c.free();
636             return result;
637         } catch (IOException e) {
638             throw new RuntimeException(e);
639         } catch (SQLException e) {
640             throw new SQLRuntimeException(e);
641         }
642     }
643 
644     /**
645      * Automatically frees the blob (<code>blob.free()</code>) once the blob
646      * {@link InputStream} is closed.
647      * 
648      * @param blob
649      * @param is
650      * @return
651      */
652     private static InputStream createFreeOnCloseInputStream(final Blob blob, final InputStream is) {
653         return new InputStream() {
654 
655             @Override
656             public int read() throws IOException {
657                 return is.read();
658             }
659 
660             @Override
661             public void close() throws IOException {
662                 try {
663                     is.close();
664                 } finally {
665                     try {
666                         blob.free();
667                     } catch (SQLException e) {
668                         log.debug(e.getMessage());
669                     }
670                 }
671             }
672         };
673     }
674 
675     /**
676      * Automatically frees the clob (<code>Clob.free()</code>) once the clob Reader
677      * is closed.
678      * 
679      * @param clob
680      * @param reader
681      * @return
682      */
683     private static Reader createFreeOnCloseReader(final Clob clob, final Reader reader) {
684         return new Reader() {
685 
686             @Override
687             public void close() throws IOException {
688                 try {
689                     reader.close();
690                 } finally {
691                     try {
692                         clob.free();
693                     } catch (SQLException e) {
694                         log.debug(e.getMessage());
695                     }
696                 }
697             }
698 
699             @Override
700             public int read(char[] cbuf, int off, int len) throws IOException {
701                 return reader.read(cbuf, off, len);
702             }
703         };
704     }
705 
706     /**
707      * Returns a function that converts the ResultSet column values into parameters
708      * to the constructor (with number of parameters equals the number of columns)
709      * of type <code>cls</code> then returns an instance of type <code>cls</code>.
710      * See {@link SelectBuilder#autoMap(Class)}.
711      * 
712      * @param cls
713      * @return
714      */
715     static <T> ResultSetMapper<T> autoMap(Class<T> cls) {
716         return new ResultSetMapper<T>() {
717 
718             ProxyService<T> proxyService;
719             private ResultSet rs;
720 
721             @Override
722             public T apply(ResultSet rs) {
723                 // access to this method will be serialized
724                 // only create a new ProxyService when the ResultSet changes
725                 // for example with the second run of a PreparedStatement
726                 if (rs != this.rs) {
727                     this.rs = rs;
728                     proxyService = new ProxyService<T>(rs, cls);
729                 }
730                 return autoMap(rs, cls, proxyService);
731             }
732         };
733     }
734 
735     /**
736      * Converts the ResultSet column values into parameters to the constructor (with
737      * number of parameters equals the number of columns) of type <code>T</code>
738      * then returns an instance of type <code>T</code>. See See
739      * {@link SelectBuilder#autoMap(Class)}.
740      * 
741      * @param rs
742      * @param cls
743      *            the class of the resultant instance
744      * @param proxyService
745      * @return an automapped instance
746      */
747     static <T> T autoMap(ResultSet rs, Class<T> cls, ProxyService<T> proxyService) {
748         return proxyService.newInstance();
749     }
750 
751     interface Col {
752         Class<?> returnType();
753     }
754 
755     static class NamedCol implements Col {
756         final String name;
757         private final Class<?> returnType;
758 
759         public NamedCol(String name, Class<?> returnType) {
760             this.name = name;
761             this.returnType = returnType;
762         }
763 
764         @Override
765         public Class<?> returnType() {
766             return returnType;
767         }
768     }
769 
770     static class IndexedCol implements Col {
771         final int index;
772         private final Class<?> returnType;
773 
774         public IndexedCol(int index, Class<?> returnType) {
775             this.index = index;
776             this.returnType = returnType;
777         }
778 
779         @Override
780         public Class<?> returnType() {
781             return returnType;
782         }
783 
784     }
785 
786     private static class ProxyService<T> {
787 
788         private final Map<String, Integer> colIndexes;
789         private final Map<String, Col> methodCols;
790         private final Class<T> cls;
791         private final ResultSet rs;
792 
793         public ProxyService(ResultSet rs, Class<T> cls) {
794             this(rs, collectColIndexes(rs), getMethodCols(cls), cls);
795         }
796 
797         public ProxyService(ResultSet rs, Map<String, Integer> colIndexes, Map<String, Col> methodCols, Class<T> cls) {
798             this.rs = rs;
799             this.colIndexes = colIndexes;
800             this.methodCols = methodCols;
801             this.cls = cls;
802         }
803 
804         private Map<String, Object> values() {
805             Map<String, Object> values = new HashMap<String, Object>();
806             // calculate values for all the interface methods and put them in a
807             // map
808             for (Method m : cls.getMethods()) {
809                 String methodName = m.getName();
810                 Col column = methodCols.get(methodName);
811                 if (column != null) {
812                     Integer index;
813                     if (column instanceof NamedCol) {
814                         String name = ((NamedCol) column).name;
815                         index = colIndexes.get(name.toUpperCase(Locale.ENGLISH));
816                         if (index == null) {
817                             throw new ColumnNotFoundException("query column names do not include '" + name
818                                     + "' which is a named column in the automapped interface " + cls.getName());
819                         }
820                     } else {
821                         IndexedCol col = (IndexedCol) column;
822                         index = col.index;
823                         if (index < 1) {
824                             throw new ColumnIndexOutOfRangeException(
825                                     "value for Index annotation (on autoMapped interface " + cls.getName()
826                                             + ") must be > 0");
827                         } else {
828                             int count = getColumnCount(rs);
829                             if (index > count) {
830                                 throw new ColumnIndexOutOfRangeException("value " + index
831                                         + " for Index annotation (on autoMapped interface " + cls.getName()
832                                         + ") must be between 1 and the number of columns in the result set (" + count
833                                         + ")");
834                             }
835                         }
836                     }
837                     Object value = autoMap(getObject(rs, column.returnType(), index), column.returnType());
838                     values.put(methodName, value);
839                 }
840             }
841             if (values.isEmpty()) {
842                 throw new AnnotationsNotFoundException(
843                         "Did you forget to add @Column or @Index annotations to " + cls.getName() + "?");
844             }
845             return values;
846         }
847 
848         @SuppressWarnings("unchecked")
849         public T newInstance() {
850             return (T) Proxy.newProxyInstance(cls.getClassLoader(), new Class[] { cls },
851                     new ProxyInstance<T>(cls, values()));
852         }
853 
854     }
855 
856     private static int getColumnCount(ResultSet rs) {
857         try {
858             return rs.getMetaData().getColumnCount();
859         } catch (SQLException e) {
860             throw new SQLRuntimeException(e);
861         }
862     }
863 
864     private static final class ProxyInstance<T> implements java.lang.reflect.InvocationHandler {
865 
866         private static boolean JAVA_9 = false;
867 
868         private static final String METHOD_TO_STRING = "toString";
869 
870         private final Class<T> cls;
871         private final Map<String, Object> values;
872 
873         ProxyInstance(Class<T> cls, Map<String, Object> values) {
874             this.cls = cls;
875             this.values = values;
876         }
877 
878         @Override
879         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
880             if (METHOD_TO_STRING.equals(method.getName()) && isEmpty(args)) {
881                 return Util.toString(cls.getSimpleName(), values);
882             } else if ("equals".equals(method.getName()) && args != null && args.length == 1) {
883                 if (args[0] == null) {
884                     return false;
885                 } else if (args[0] instanceof Proxy) {
886                     ProxyInstance<?> handler = (ProxyInstance<?>) Proxy.getInvocationHandler(args[0]);
887                     if (!handler.cls.equals(cls)) {
888                         // is a proxied object for a different interface!
889                         return false;
890                     } else {
891                         return handler.values.equals(values);
892                     }
893                 } else {
894                     return false;
895                 }
896             } else if (isHashCode(method, args)) {
897                 return values.hashCode();
898             } else if (values.containsKey(method.getName()) && isEmpty(args)) {
899                 return values.get(method.getName());
900             } else if (method.isDefault()) {
901                 final Class<?> declaringClass = method.getDeclaringClass();
902                 if (!Modifier.isPublic(declaringClass.getModifiers())) {
903                     throw new AutomappedInterfaceInaccessibleException(
904                             "An automapped interface must be public for you to call default methods on that interface");
905                 }
906                 // TODO java 9 support (fix IllegalAccessException)
907                 if (JAVA_9) {
908                     MethodType methodType = MethodType.methodType(method.getReturnType(), method.getParameterTypes());
909                     return MethodHandles.lookup() //
910                             .findSpecial( //
911                                     declaringClass, //
912                                     method.getName(), //
913                                     methodType, //
914                                     declaringClass) //
915                             .bindTo(proxy) //
916                             .invoke(args);
917                 } else {
918                     Constructor<MethodHandles.Lookup> constructor = //
919                             MethodHandles.Lookup.class //
920                                     .getDeclaredConstructor(Class.class, int.class);
921                     constructor.setAccessible(true);
922                     return constructor //
923                             .newInstance(declaringClass, MethodHandles.Lookup.PRIVATE)
924                             .unreflectSpecial(method, declaringClass) //
925                             .bindTo(proxy) //
926                             .invokeWithArguments(args);
927                 }
928             } else {
929                 throw new RuntimeException("unexpected");
930             }
931         }
932 
933     }
934 
935     @VisibleForTesting
936     static boolean isHashCode(Method method, Object[] args) {
937         return "hashCode".equals(method.getName()) && isEmpty(args);
938     }
939 
940     private static boolean isEmpty(Object[] args) {
941         return args == null || args.length == 0;
942     }
943 
944     private static String toString(String clsSimpleName, Map<String, Object> values) {
945         StringBuilder s = new StringBuilder();
946         s.append(clsSimpleName);
947         s.append("[");
948         boolean first = true;
949         for (Entry<String, Object> entry : new TreeMap<String, Object>(values).entrySet()) {
950             if (!first) {
951                 s.append(", ");
952             }
953             s.append(entry.getKey());
954             s.append("=");
955             s.append(entry.getValue());
956             first = false;
957         }
958         s.append("]");
959         return s.toString();
960     }
961 
962     private static Map<String, Col> getMethodCols(Class<?> cls) {
963         Map<String, Col> methodCols = new HashMap<String, Col>();
964         for (Method method : cls.getMethods()) {
965             String name = method.getName();
966             Column column = method.getAnnotation(Column.class);
967             if (column != null) {
968                 checkHasNoParameters(method);
969                 // TODO check method has a mappable return type
970                 String col = column.value();
971                 if (col.equals(Column.NOT_SPECIFIED))
972                     col = Util.camelCaseToUnderscore(name);
973                 methodCols.put(name, new NamedCol(col, method.getReturnType()));
974             } else {
975                 Index index = method.getAnnotation(Index.class);
976                 if (index != null) {
977                     // TODO check method has a mappable return type
978                     checkHasNoParameters(method);
979                     methodCols.put(name, new IndexedCol(index.value(), method.getReturnType()));
980                 }
981             }
982         }
983         return methodCols;
984     }
985 
986     private static void checkHasNoParameters(Method method) {
987         if (method.getParameterTypes().length > 0) {
988             throw new RuntimeException("mapped interface method cannot have parameters");
989         }
990     }
991 
992     private static Map<String, Integer> collectColIndexes(ResultSet rs) {
993         HashMap<String, Integer> map = new HashMap<String, Integer>();
994         try {
995             ResultSetMetaData metadata = rs.getMetaData();
996             for (int i = 1; i <= metadata.getColumnCount(); i++) {
997                 map.put(metadata.getColumnName(i).toUpperCase(Locale.ENGLISH), i);
998             }
999             return map;
1000         } catch (SQLException e) {
1001             throw new SQLRuntimeException(e);
1002         }
1003     }
1004 
1005     static String camelCaseToUnderscore(String camelCased) {
1006         // guava has best solution for this with CaseFormat class
1007         // but don't want to add dependency just for this method
1008         final @RegEx String regex = "([a-z])([A-Z]+)";
1009         final String replacement = "$1_$2";
1010         return camelCased.replaceAll(regex, replacement);
1011     }
1012 
1013     public static ConnectionProvider connectionProvider(String url, Properties properties) {
1014         return new ConnectionProvider() {
1015 
1016             @Override
1017             public Connection get() {
1018                 try {
1019                     return DriverManager.getConnection(url, properties);
1020                 } catch (SQLException e) {
1021                     throw new SQLRuntimeException(e);
1022                 }
1023             }
1024 
1025             @Override
1026             public void close() {
1027                 //
1028             }
1029         };
1030     }
1031 
1032     static Connection toTransactedConnection(AtomicReference<Connection> connection, Connection c) throws SQLException {
1033         if (c instanceof TransactedConnection) {
1034             connection.set(c);
1035             return c;
1036         } else {
1037             c.setAutoCommit(false);
1038             log.debug("creating new TransactedConnection");
1039             TransactedConnection c2 = new TransactedConnection(c);
1040             connection.set(c2);
1041             return c2;
1042         }
1043     }
1044 
1045     public static ConnectionProvider connectionProvider(DataSource dataSource) {
1046         return new ConnectionProvider() {
1047 
1048             @Override
1049             public Connection get() {
1050                 return getConnection(dataSource);
1051             }
1052 
1053             @Override
1054             public void close() {
1055                 // do nothing
1056             }
1057         };
1058     }
1059 
1060     @VisibleForTesting
1061     static Connection getConnection(DataSource ds) {
1062         try {
1063             return ds.getConnection();
1064         } catch (SQLException e) {
1065             throw new SQLRuntimeException(e);
1066         }
1067     }
1068 
1069     public static boolean hasCollection(List<Parameter> params) {
1070         return params.stream().anyMatch(x -> x.isCollection());
1071     }
1072 
1073 }