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 23KB

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