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
72
73
74
75
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
149
150
151
152
153
154
155
156 private static void setBlob(PreparedStatement ps, int i, Object o, Class<?> cls) throws SQLException {
157
158 ps.setBlob(i, (Blob) o);
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
184 private static void setClob(PreparedStatement ps, int i, Object o, Class<?> cls) throws SQLException {
185
186 ps.setClob(i, (Clob) o);
187
188
189
190
191
192
193
194
195
196
197
198
199
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
298
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
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
344 static NamedCallableStatement prepareCall(Connection con, int fetchSize, String sql,
345 List<ParameterPlaceholder> parameterPlaceholders) throws SQLException {
346
347
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
379
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
422
423
424
425
426
427
428
429
430
431
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
541
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
574
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
602
603
604
605
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
625
626
627
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
646
647
648
649
650
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
677
678
679
680
681
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
708
709
710
711
712
713
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
724
725
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
737
738
739
740
741
742
743
744
745
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
807
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
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
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
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
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
1007
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
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 }