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