You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DaoProxy.java 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. /*
  2. * Copyright 2014 James Moger.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package com.iciql;
  17. import java.lang.annotation.Annotation;
  18. import java.lang.reflect.Array;
  19. import java.lang.reflect.Constructor;
  20. import java.lang.reflect.Field;
  21. import java.lang.reflect.InvocationHandler;
  22. import java.lang.reflect.InvocationTargetException;
  23. import java.lang.reflect.Method;
  24. import java.lang.reflect.Modifier;
  25. import java.lang.reflect.Proxy;
  26. import java.sql.ResultSet;
  27. import java.sql.SQLException;
  28. import java.util.Collection;
  29. import java.util.Collections;
  30. import java.util.HashSet;
  31. import java.util.List;
  32. import java.util.Map;
  33. import java.util.Set;
  34. import java.util.TreeMap;
  35. import java.util.concurrent.ConcurrentHashMap;
  36. import java.util.regex.Matcher;
  37. import java.util.regex.Pattern;
  38. import com.iciql.Iciql.DataTypeAdapter;
  39. import com.iciql.util.JdbcUtils;
  40. import com.iciql.util.StringUtils;
  41. import com.iciql.util.Utils;
  42. /**
  43. * DaoProxy creates a dynamic instance of the provided Dao interface.
  44. *
  45. * @author James Moger
  46. *
  47. * @param <X>
  48. */
  49. final class DaoProxy<X extends Dao> implements InvocationHandler, Dao {
  50. private final Db db;
  51. private final Class<X> daoInterface;
  52. private final char bindingDelimiter = ':';
  53. private final Map<Method, IndexedSql> indexedSqlCache;
  54. DaoProxy(Db db, Class<X> daoInterface) {
  55. this.db = db;
  56. this.daoInterface = daoInterface;
  57. this.indexedSqlCache = new ConcurrentHashMap<Method, IndexedSql>();
  58. }
  59. /**
  60. * Builds a proxy object for the DAO interface.
  61. *
  62. * @return a proxy object
  63. */
  64. @SuppressWarnings("unchecked")
  65. X build() {
  66. if (!daoInterface.isInterface()) {
  67. throw new IciqlException("Dao {0} must be an interface!", daoInterface.getName());
  68. }
  69. ClassLoader classLoader = daoInterface.getClassLoader();
  70. Set<Class<?>> interfaces = new HashSet<Class<?>>();
  71. interfaces.add(Dao.class);
  72. interfaces.add(daoInterface);
  73. for (Class<?> clazz : daoInterface.getInterfaces()) {
  74. interfaces.add(clazz);
  75. }
  76. Class<?>[] constructorParams = { InvocationHandler.class };
  77. Class<?>[] allInterfaces = interfaces.toArray(new Class<?>[interfaces.size()]);
  78. try {
  79. Class<?> proxyClass = Proxy.getProxyClass(classLoader, allInterfaces);
  80. Constructor<?> proxyConstructor = proxyClass.getConstructor(constructorParams);
  81. return (X) proxyConstructor.newInstance(new Object[] { this });
  82. } catch (Exception e) {
  83. throw new IciqlException(e);
  84. }
  85. }
  86. /**
  87. * Invoke intercepts method calls and delegates execution to the appropriate object.
  88. */
  89. @Override
  90. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  91. try {
  92. if (method.getDeclaringClass() == Dao.class) {
  93. return method.invoke(this, args);
  94. } else if (method.isAnnotationPresent(SqlQuery.class)) {
  95. String sql = method.getAnnotation(SqlQuery.class).value();
  96. String statement = db.getDaoStatementProvider().getStatement(sql, db.getMode());
  97. return executeQuery(method, args, statement);
  98. } else if (method.isAnnotationPresent(SqlStatement.class)) {
  99. String sql = method.getAnnotation(SqlStatement.class).value();
  100. String statement = db.getDaoStatementProvider().getStatement(sql, db.getMode());
  101. return executeStatement(method, args, statement);
  102. } else {
  103. throw new IciqlException("Can not invoke non-dao method {0}.{1}",
  104. method.getDeclaringClass().getSimpleName(), method.getName());
  105. }
  106. } catch (InvocationTargetException te) {
  107. throw te.getCause();
  108. }
  109. }
  110. /**
  111. * Execute a query.
  112. *
  113. * @param method
  114. * @param methodArgs
  115. * @param sql
  116. * @return the result
  117. */
  118. private Object executeQuery(Method method, Object[] methodArgs, String sql) {
  119. /*
  120. * Determine and validate the return type
  121. */
  122. Class<?> returnType = method.getReturnType();
  123. if (void.class == returnType) {
  124. throw new IciqlException("You must specify a return type for @{0} {1}.{2}!",
  125. SqlQuery.class.getSimpleName(), method.getDeclaringClass().getSimpleName(), method.getName());
  126. }
  127. if (Collection.class.isAssignableFrom(returnType)) {
  128. throw new IciqlException("You may not return a collection for an @{0} method, please change the return type of {1}.{2} to YourClass[]!",
  129. SqlQuery.class.getSimpleName(), method.getDeclaringClass().getSimpleName(), method.getName());
  130. }
  131. boolean isArray = false;
  132. if (returnType.isArray()) {
  133. isArray = true;
  134. returnType = returnType.getComponentType();
  135. }
  136. boolean isJavaType = returnType.isEnum()
  137. || returnType.isPrimitive()
  138. || java.lang.Boolean.class.isAssignableFrom(returnType)
  139. || java.lang.Number.class.isAssignableFrom(returnType)
  140. || java.lang.String.class.isAssignableFrom(returnType)
  141. || java.util.Date.class.isAssignableFrom(returnType)
  142. || byte[].class.isAssignableFrom(returnType);
  143. Class<? extends DataTypeAdapter<?>> adapter = Utils.getDataTypeAdapter(method.getAnnotations());
  144. if (adapter == null) {
  145. adapter = Utils.getDataTypeAdapter(returnType.getAnnotations());
  146. }
  147. /*
  148. * Prepare & execute sql
  149. */
  150. PreparedSql preparedSql = prepareSql(method, methodArgs, sql);
  151. List<Object> objects;
  152. if (!isJavaType && adapter == null) {
  153. // query of an Iciql model
  154. objects = db.executeQuery(returnType, preparedSql.sql, preparedSql.parameters);
  155. } else {
  156. // query of (array of) standard Java type or a DataTypeAdapter type
  157. objects = Utils.newArrayList();
  158. ResultSet rs = db.executeQuery(preparedSql.sql, preparedSql.parameters);
  159. try {
  160. while (rs.next()) {
  161. Object value = db.getDialect().deserialize(rs, 1, returnType, adapter);
  162. objects.add(value);
  163. if (!isArray) {
  164. // we are not returning an array so we break
  165. // the loop and return the first result
  166. break;
  167. }
  168. }
  169. } catch (SQLException e) {
  170. throw new IciqlException(e);
  171. } finally {
  172. JdbcUtils.closeSilently(rs);
  173. }
  174. }
  175. /*
  176. * Return the results
  177. */
  178. if (objects == null || objects.isEmpty()) {
  179. // no results
  180. if (isArray) {
  181. // return an empty array
  182. return Array.newInstance(returnType, 0);
  183. }
  184. // nothing to return!
  185. return null;
  186. } else if (isArray) {
  187. // return an array of object results
  188. Object array = Array.newInstance(returnType, objects.size());
  189. for (int i = 0; i < objects.size(); i++) {
  190. Array.set(array, i, objects.get(i));
  191. }
  192. return array;
  193. }
  194. // return first element
  195. return objects.get(0);
  196. }
  197. /**
  198. * Execute a statement.
  199. *
  200. * @param method
  201. * @param methodArgs
  202. * @param sql
  203. * @return the result
  204. */
  205. private Object executeStatement(Method method, Object[] methodArgs, String sql) {
  206. /*
  207. * Determine and validate the return type
  208. */
  209. Class<?> returnType = method.getReturnType();
  210. if (void.class != returnType && boolean.class != returnType && int.class != returnType) {
  211. throw new IciqlException("Invalid return type '{0}' for @{1} {2}.{3}!",
  212. returnType.getSimpleName(), SqlQuery.class.getSimpleName(),
  213. method.getDeclaringClass().getSimpleName(), method.getName());
  214. }
  215. /*
  216. * Prepare & execute sql
  217. */
  218. PreparedSql preparedSql = prepareSql(method, methodArgs, sql);
  219. int rows = db.executeUpdate(preparedSql.sql, preparedSql.parameters);
  220. /*
  221. * Return the results
  222. */
  223. if (void.class == returnType) {
  224. // return nothing
  225. return null;
  226. } else if (boolean.class == returnType) {
  227. // return true if any rows were affected
  228. return rows > 0;
  229. } else {
  230. // return number of rows
  231. return rows;
  232. }
  233. }
  234. /**
  235. * Prepares an sql statement and execution parameters based on the supplied
  236. * method and it's arguments.
  237. *
  238. * @param method
  239. * @param methodArgs
  240. * @param sql
  241. * @return a prepared sql statement and arguments
  242. */
  243. private PreparedSql prepareSql(Method method, Object[] methodArgs, String sql) {
  244. if (methodArgs == null || methodArgs.length == 0) {
  245. // no method arguments
  246. return new PreparedSql(sql, null);
  247. }
  248. IndexedSql indexedSql = indexedSqlCache.get(method);
  249. if (indexedSql == null) {
  250. // index the sql and method args
  251. indexedSql = indexSql(method, sql);
  252. // cache the indexed sql for re-use
  253. indexedSqlCache.put(method, indexedSql);
  254. }
  255. final PreparedSql preparedSql = indexedSql.prepareSql(db, methodArgs);
  256. return preparedSql;
  257. }
  258. /**
  259. * Indexes an sql statement and method args based on the supplied
  260. * method and it's arguments.
  261. *
  262. * @param method
  263. * @param sql
  264. * @return an indexed sql statement and arguments
  265. */
  266. private IndexedSql indexSql(Method method, String sql) {
  267. Map<String, IndexedArgument> parameterIndex = buildParameterIndex(method);
  268. // build a regex to extract parameter names from the sql statement
  269. StringBuilder sb = new StringBuilder();
  270. sb.append(bindingDelimiter);
  271. sb.append("{1}(\\?");
  272. for (String name : parameterIndex.keySet()) {
  273. sb.append("|");
  274. // strip binding delimeter from name
  275. sb.append(name);
  276. }
  277. sb.append(')');
  278. // identify parameters, replace with the '?' PreparedStatement
  279. // delimiter and build the PreparedStatement parameters array
  280. final String regex = sb.toString();
  281. final Pattern p = Pattern.compile(regex);
  282. final Matcher m = p.matcher(sql);
  283. final StringBuffer buffer = new StringBuffer();
  284. List<IndexedArgument> indexedArgs = Utils.newArrayList();
  285. int count = 0;
  286. while (m.find()) {
  287. String binding = m.group(1);
  288. m.appendReplacement(buffer, "?");
  289. IndexedArgument indexedArg;
  290. if ("?".equals(binding)) {
  291. // standard ? JDBC placeholder
  292. indexedArg = parameterIndex.get("arg" + count);
  293. } else {
  294. // named placeholder
  295. indexedArg = parameterIndex.get(binding);
  296. }
  297. if (indexedArg == null) {
  298. throw new IciqlException("Unbound SQL parameter '{0}' in {1}.{2}",
  299. binding, method.getDeclaringClass().getSimpleName(), method.getName());
  300. }
  301. indexedArgs.add(indexedArg);
  302. count++;
  303. }
  304. m.appendTail(buffer);
  305. final String statement = buffer.toString();
  306. // create an IndexedSql container for the statement and indexes
  307. return new IndexedSql(statement, Collections.unmodifiableList(indexedArgs));
  308. }
  309. /**
  310. * Builds an index of parameter name->(position,typeAdapter) from the method arguments
  311. * array. This index is calculated once per method.
  312. *
  313. * @param method
  314. * @return a bindings map of ("name", IndexedArgument) pairs
  315. */
  316. private Map<String, IndexedArgument> buildParameterIndex(Method method) {
  317. Map<String, IndexedArgument> index = new TreeMap<String, IndexedArgument>();
  318. Annotation [][] annotationsMatrix = method.getParameterAnnotations();
  319. for (int i = 0; i < annotationsMatrix.length; i++) {
  320. Annotation [] annotations = annotationsMatrix[i];
  321. /*
  322. * Conditionally map the bean properties of the method argument
  323. * class to Method and Field instances.
  324. */
  325. BindBean bean = getAnnotation(BindBean.class, annotations);
  326. if (bean != null) {
  327. final String prefix = bean.value();
  328. final Class<?> argumentClass = method.getParameterTypes()[i];
  329. Map<String, IndexedArgument> beanIndex = buildBeanIndex(i, prefix, argumentClass);
  330. index.putAll(beanIndex);
  331. }
  332. Class<? extends DataTypeAdapter<?>> typeAdapter = Utils.getDataTypeAdapter(annotations);
  333. final IndexedArgument indexedArgument = new IndexedArgument(i, typeAdapter);
  334. // :N - 1-indexed, like JDBC ResultSet
  335. index.put("" + (i + 1), indexedArgument);
  336. // argN - 0-indexed, like Reflection
  337. index.put("arg" + i, indexedArgument);
  338. // Bound name
  339. Bind binding = getAnnotation(Bind.class, annotations);
  340. if (binding!= null && !binding.value().isEmpty()) {
  341. index.put(binding.value(), indexedArgument);
  342. }
  343. // try mapping Java 8 argument names, may overwrite argN
  344. try {
  345. Class<?> nullArgs = null;
  346. Method getParameters = method.getClass().getMethod("getParameters", nullArgs);
  347. if (getParameters != null) {
  348. Object [] parameters = (Object []) getParameters.invoke(method, nullArgs);
  349. if (parameters != null) {
  350. Object o = parameters[i];
  351. Method getName = o.getClass().getMethod("getName", nullArgs);
  352. String j8name = getName.invoke(o, nullArgs).toString();
  353. if (!j8name.isEmpty()) {
  354. index.put(j8name, indexedArgument);
  355. }
  356. }
  357. }
  358. } catch (Throwable t) {
  359. }
  360. }
  361. return index;
  362. }
  363. /**
  364. * Builds an index of parameter name->(position,method) from the method arguments
  365. * array. This index is calculated once per method.
  366. *
  367. * @param argumentIndex
  368. * @param prefix
  369. * @param beanClass
  370. * @return a bindings map of ("prefix.property", IndexedArgument) pairs
  371. */
  372. private Map<String, IndexedArgument> buildBeanIndex(int argumentIndex, String prefix, Class<?> beanClass) {
  373. final String beanPrefix = StringUtils.isNullOrEmpty(prefix) ? "" : (prefix + ".");
  374. final Map<String, IndexedArgument> index = new TreeMap<String, IndexedArgument>();
  375. // map JavaBean property getters
  376. for (Method method : beanClass.getMethods()) {
  377. if (Modifier.isStatic(method.getModifiers())
  378. || method.getReturnType() == void.class
  379. || method.getParameterTypes().length > 0
  380. || method.getDeclaringClass() == Object.class) {
  381. // not a JavaBean property
  382. continue;
  383. }
  384. final String propertyName;
  385. final String name = method.getName();
  386. if (name.startsWith("get")) {
  387. propertyName = method.getName().substring(3);
  388. } else if (name.startsWith("is")) {
  389. propertyName = method.getName().substring(2);
  390. } else {
  391. propertyName = null;
  392. }
  393. if (propertyName == null) {
  394. // not a conventional JavaBean property
  395. continue;
  396. }
  397. final String binding = beanPrefix + preparePropertyName(propertyName);
  398. final IndexedArgument indexedArg = new IndexedArgument(argumentIndex, method);
  399. index.put(binding, indexedArg);
  400. }
  401. // map public instance fields
  402. for (Field field : beanClass.getFields()) {
  403. if (Modifier.isStatic(field.getModifiers())) {
  404. // not a JavaBean property
  405. continue;
  406. }
  407. final String binding = beanPrefix + preparePropertyName(field.getName());
  408. final IndexedArgument indexedArg = new IndexedArgument(argumentIndex, field);
  409. index.put(binding, indexedArg);
  410. }
  411. return index;
  412. }
  413. @SuppressWarnings("unchecked")
  414. private <T> T getAnnotation(Class<T> annotationClass, Annotation [] annotations) {
  415. if (annotations != null) {
  416. for (Annotation annotation : annotations) {
  417. if (annotation.annotationType() == annotationClass) {
  418. return (T) annotation;
  419. }
  420. }
  421. }
  422. return null;
  423. }
  424. private String preparePropertyName(String value) {
  425. return Character.toLowerCase(value.charAt(0)) + value.substring(1);
  426. }
  427. /*
  428. *
  429. * Standard Dao method implementations delegate to the underlying Db
  430. *
  431. */
  432. @Override
  433. public final Db db() {
  434. return db;
  435. }
  436. @Override
  437. public final <T> boolean insert(T t) {
  438. return db.insert(t);
  439. }
  440. @Override
  441. public final <T> void insertAll(List<T> t) {
  442. db.insertAll(t);
  443. }
  444. @Override
  445. public final <T> long insertAndGetKey(T t) {
  446. return db.insertAndGetKey(t);
  447. }
  448. @Override
  449. public final <T> List<Long> insertAllAndGetKeys(List<T> t) {
  450. return db.insertAllAndGetKeys(t);
  451. }
  452. @Override
  453. public final <T> boolean update(T t) {
  454. return db.update(t);
  455. }
  456. @Override
  457. public final <T> void updateAll(List<T> t) {
  458. db.updateAll(t);
  459. }
  460. @Override
  461. public final <T> void merge(T t) {
  462. db.merge(t);
  463. }
  464. @Override
  465. public final <T> boolean delete(T t) {
  466. return db.delete(t);
  467. }
  468. @Override
  469. public final <T> void deleteAll(List<T> t) {
  470. db.deleteAll(t);
  471. }
  472. @Override
  473. public final void close() {
  474. db.close();
  475. }
  476. /**
  477. * Container class to hold the prepared JDBC SQL statement and execution
  478. * parameters.
  479. */
  480. private class PreparedSql {
  481. final String sql;
  482. final Object [] parameters;
  483. PreparedSql(String sql, Object [] parameters) {
  484. this.sql = sql;
  485. this.parameters = parameters;
  486. }
  487. @Override
  488. public String toString() {
  489. return sql;
  490. }
  491. }
  492. /**
  493. * Container class to hold a parsed JDBC SQL statement and
  494. * IndexedParameters.
  495. * <p>
  496. * Instances of this class are cached because they are functional processing
  497. * containers as they contain Method and Field references for binding beans
  498. * and matching to method arguments.
  499. * </p>
  500. */
  501. private class IndexedSql {
  502. final String sql;
  503. final List<IndexedArgument> indexedArgs;
  504. IndexedSql(String sql, List<IndexedArgument> indexedArgs) {
  505. this.sql = sql;
  506. this.indexedArgs = indexedArgs;
  507. }
  508. /**
  509. * Prepares the method arguments for statement execution.
  510. *
  511. * @param db
  512. * @param methodArgs
  513. * @return the prepared sql statement and parameters
  514. */
  515. PreparedSql prepareSql(Db db, Object [] methodArgs) {
  516. Object [] parameters = new Object[indexedArgs.size()];
  517. for (int i = 0; i < indexedArgs.size(); i++) {
  518. IndexedArgument indexedArg = indexedArgs.get(i);
  519. Object methodArg = methodArgs[indexedArg.index];
  520. Object value = methodArg;
  521. Class<? extends DataTypeAdapter<?>> typeAdapter = indexedArg.typeAdapter;
  522. if (indexedArg.method != null) {
  523. // execute the bean method
  524. try {
  525. value = indexedArg.method.invoke(methodArg);
  526. typeAdapter = Utils.getDataTypeAdapter(indexedArg.method.getAnnotations());
  527. } catch (Exception e) {
  528. throw new IciqlException(e);
  529. }
  530. } else if (indexedArg.field != null) {
  531. // extract the field value
  532. try {
  533. value = indexedArg.field.get(methodArg);
  534. typeAdapter = Utils.getDataTypeAdapter(indexedArg.field.getAnnotations());
  535. } catch (Exception e) {
  536. throw new IciqlException(e);
  537. }
  538. } else if (typeAdapter == null) {
  539. // identify the type adapter for the argument class
  540. typeAdapter = Utils.getDataTypeAdapter(methodArg.getClass().getAnnotations());
  541. }
  542. // prepare the parameter
  543. parameters[i] = db.getDialect().serialize(value, typeAdapter);
  544. }
  545. return new PreparedSql(sql, parameters);
  546. }
  547. @Override
  548. public String toString() {
  549. return sql;
  550. }
  551. }
  552. /**
  553. * IndexedArgument holds cached information about how to process an method
  554. * argument by it's index in the method arguments array.
  555. * <p>
  556. * An argument may be passed-through, might be bound to a bean property,
  557. * might be transformed with a type adapter, or a combination of these.
  558. * </p>
  559. */
  560. private class IndexedArgument {
  561. final int index;
  562. final Class<? extends DataTypeAdapter<?>> typeAdapter;
  563. final Method method;
  564. final Field field;
  565. IndexedArgument(int index, Class<? extends DataTypeAdapter<?>> typeAdapter) {
  566. this.index = index;
  567. this.typeAdapter = typeAdapter;
  568. this.method = null;
  569. this.field = null;
  570. }
  571. IndexedArgument(int methodArgIndex, Method method) {
  572. this.index = methodArgIndex;
  573. this.typeAdapter = null;
  574. this.method = method;
  575. this.field = null;
  576. }
  577. IndexedArgument(int methodArgIndex, Field field) {
  578. this.index = methodArgIndex;
  579. this.typeAdapter = null;
  580. this.method = null;
  581. this.field = field;
  582. }
  583. @Override
  584. public String toString() {
  585. String accessor;
  586. if (method != null) {
  587. accessor = "M:" + method.getDeclaringClass().getSimpleName() + "." + method.getName();
  588. } else if (field != null) {
  589. accessor = "F:" + field.getDeclaringClass().getSimpleName() + "." + field.getName();
  590. } else {
  591. accessor = "A:arg";
  592. }
  593. return index + ":" + accessor + (typeAdapter == null ? "" : (":" + typeAdapter.getSimpleName()));
  594. }
  595. }
  596. }