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
71
72
73
74
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
148
149
150
151
152
153
154
155 private static void setBlob(PreparedStatement ps, int i, Object o, Class<?> cls) throws SQLException {
156
157 ps.setBlob(i, (Blob) o);
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172 }
173
174
175
176
177
178
179
180
181
182
183 private static void setClob(PreparedStatement ps, int i, Object o, Class<?> cls) throws SQLException {
184
185 ps.setClob(i, (Clob) o);
186
187
188
189
190
191
192
193
194
195
196
197
198
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
297
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
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
343 static NamedCallableStatement prepareCall(Connection con, int fetchSize, String sql,
344 List<ParameterPlaceholder> parameterPlaceholders) throws SQLException {
345
346
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
378
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
421
422
423
424
425
426
427
428
429
430
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
540
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
573
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
601
602
603
604
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
624
625
626
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
645
646
647
648
649
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
676
677
678
679
680
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
707
708
709
710
711
712
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
723
724
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
736
737
738
739
740
741
742
743
744
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
806
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
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
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
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
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
1006
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
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 }