123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753 |
- /*
- * Copyright 2014 James Moger.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package com.iciql;
-
- import java.lang.annotation.Annotation;
- import java.lang.reflect.Array;
- import java.lang.reflect.Constructor;
- import java.lang.reflect.Field;
- import java.lang.reflect.InvocationHandler;
- import java.lang.reflect.InvocationTargetException;
- import java.lang.reflect.Method;
- import java.lang.reflect.Modifier;
- import java.lang.reflect.Proxy;
- import java.sql.ResultSet;
- import java.sql.SQLException;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.HashSet;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.TreeMap;
- import java.util.concurrent.ConcurrentHashMap;
- import java.util.regex.Matcher;
- import java.util.regex.Pattern;
-
- import com.iciql.Iciql.DataTypeAdapter;
- import com.iciql.util.JdbcUtils;
- import com.iciql.util.StringUtils;
- import com.iciql.util.Utils;
-
- /**
- * DaoProxy creates a dynamic instance of the provided Dao interface.
- *
- * @author James Moger
- *
- * @param <X>
- */
- final class DaoProxy<X extends Dao> implements InvocationHandler, Dao {
-
- private final Db db;
-
- private final Class<X> daoInterface;
-
- private final char bindingDelimiter = ':';
-
- private final Map<Method, IndexedSql> indexedSqlCache;
-
- DaoProxy(Db db, Class<X> daoInterface) {
- this.db = db;
- this.daoInterface = daoInterface;
- this.indexedSqlCache = new ConcurrentHashMap<Method, IndexedSql>();
- }
-
- /**
- * Builds a proxy object for the DAO interface.
- *
- * @return a proxy object
- */
- @SuppressWarnings("unchecked")
- X build() {
-
- if (!daoInterface.isInterface()) {
- throw new IciqlException("Dao {0} must be an interface!", daoInterface.getName());
- }
-
- ClassLoader classLoader = daoInterface.getClassLoader();
-
- Set<Class<?>> interfaces = new HashSet<Class<?>>();
- interfaces.add(Dao.class);
- interfaces.add(daoInterface);
- for (Class<?> clazz : daoInterface.getInterfaces()) {
- interfaces.add(clazz);
- }
-
- Class<?>[] constructorParams = { InvocationHandler.class };
- Class<?>[] allInterfaces = interfaces.toArray(new Class<?>[interfaces.size()]);
-
- try {
-
- Class<?> proxyClass = Proxy.getProxyClass(classLoader, allInterfaces);
- Constructor<?> proxyConstructor = proxyClass.getConstructor(constructorParams);
- return (X) proxyConstructor.newInstance(new Object[] { this });
-
- } catch (Exception e) {
- throw new IciqlException(e);
- }
- }
-
- /**
- * Invoke intercepts method calls and delegates execution to the appropriate object.
- */
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- try {
-
- if (method.getDeclaringClass() == Dao.class) {
-
- return method.invoke(this, args);
-
- } else if (method.isAnnotationPresent(SqlQuery.class)) {
-
- String sql = method.getAnnotation(SqlQuery.class).value();
- String statement = db.getDaoStatementProvider().getStatement(sql, db.getMode());
- return executeQuery(method, args, statement);
-
- } else if (method.isAnnotationPresent(SqlStatement.class)) {
-
- String sql = method.getAnnotation(SqlStatement.class).value();
- String statement = db.getDaoStatementProvider().getStatement(sql, db.getMode());
- return executeStatement(method, args, statement);
-
- } else {
-
- throw new IciqlException("Can not invoke non-dao method {0}.{1}",
- method.getDeclaringClass().getSimpleName(), method.getName());
-
- }
-
- } catch (InvocationTargetException te) {
- throw te.getCause();
- }
- }
-
- /**
- * Execute a query.
- *
- * @param method
- * @param methodArgs
- * @param sql
- * @return the result
- */
- private Object executeQuery(Method method, Object[] methodArgs, String sql) {
-
- /*
- * Determine and validate the return type
- */
- Class<?> returnType = method.getReturnType();
-
- if (void.class == returnType) {
- throw new IciqlException("You must specify a return type for @{0} {1}.{2}!",
- SqlQuery.class.getSimpleName(), method.getDeclaringClass().getSimpleName(), method.getName());
- }
-
- if (Collection.class.isAssignableFrom(returnType)) {
- throw new IciqlException("You may not return a collection for an @{0} method, please change the return type of {1}.{2} to YourClass[]!",
- SqlQuery.class.getSimpleName(), method.getDeclaringClass().getSimpleName(), method.getName());
- }
-
- boolean isArray = false;
- if (returnType.isArray()) {
- isArray = true;
- returnType = returnType.getComponentType();
- }
-
- boolean isJavaType = returnType.isEnum()
- || returnType.isPrimitive()
- || java.lang.Boolean.class.isAssignableFrom(returnType)
- || java.lang.Number.class.isAssignableFrom(returnType)
- || java.lang.String.class.isAssignableFrom(returnType)
- || java.util.Date.class.isAssignableFrom(returnType)
- || byte[].class.isAssignableFrom(returnType);
-
- Class<? extends DataTypeAdapter<?>> adapter = Utils.getDataTypeAdapter(method.getAnnotations());
- if (adapter == null) {
- adapter = Utils.getDataTypeAdapter(returnType.getAnnotations());
- }
-
- /*
- * Prepare & execute sql
- */
- PreparedSql preparedSql = prepareSql(method, methodArgs, sql);
-
- List<Object> objects;
- if (!isJavaType && adapter == null) {
-
- // query of an Iciql model
- objects = db.executeQuery(returnType, preparedSql.sql, preparedSql.parameters);
-
- } else {
-
- // query of (array of) standard Java type or a DataTypeAdapter type
- objects = Utils.newArrayList();
- ResultSet rs = db.executeQuery(preparedSql.sql, preparedSql.parameters);
- try {
-
- while (rs.next()) {
-
- Object value = db.getDialect().deserialize(rs, 1, returnType, adapter);
- objects.add(value);
-
- if (!isArray) {
- // we are not returning an array so we break
- // the loop and return the first result
- break;
- }
- }
-
- } catch (SQLException e) {
- throw new IciqlException(e);
- } finally {
- JdbcUtils.closeSilently(rs);
- }
-
- }
-
- /*
- * Return the results
- */
- if (objects == null || objects.isEmpty()) {
-
- // no results
- if (isArray) {
- // return an empty array
- return Array.newInstance(returnType, 0);
- }
-
- // nothing to return!
- return null;
-
- } else if (isArray) {
-
- // return an array of object results
- Object array = Array.newInstance(returnType, objects.size());
- for (int i = 0; i < objects.size(); i++) {
- Array.set(array, i, objects.get(i));
- }
- return array;
-
- }
-
- // return first element
- return objects.get(0);
- }
-
-
- /**
- * Execute a statement.
- *
- * @param method
- * @param methodArgs
- * @param sql
- * @return the result
- */
- private Object executeStatement(Method method, Object[] methodArgs, String sql) {
-
- /*
- * Determine and validate the return type
- */
- Class<?> returnType = method.getReturnType();
-
- if (void.class != returnType && boolean.class != returnType && int.class != returnType) {
-
- throw new IciqlException("Invalid return type '{0}' for @{1} {2}.{3}!",
- returnType.getSimpleName(), SqlQuery.class.getSimpleName(),
- method.getDeclaringClass().getSimpleName(), method.getName());
- }
-
- /*
- * Prepare & execute sql
- */
- PreparedSql preparedSql = prepareSql(method, methodArgs, sql);
- int rows = db.executeUpdate(preparedSql.sql, preparedSql.parameters);
-
- /*
- * Return the results
- */
- if (void.class == returnType) {
-
- // return nothing
- return null;
-
- } else if (boolean.class == returnType) {
-
- // return true if any rows were affected
- return rows > 0;
-
- } else {
-
- // return number of rows
- return rows;
-
- }
- }
-
- /**
- * Prepares an sql statement and execution parameters based on the supplied
- * method and it's arguments.
- *
- * @param method
- * @param methodArgs
- * @param sql
- * @return a prepared sql statement and arguments
- */
- private PreparedSql prepareSql(Method method, Object[] methodArgs, String sql) {
-
- if (methodArgs == null || methodArgs.length == 0) {
- // no method arguments
- return new PreparedSql(sql, null);
- }
-
- IndexedSql indexedSql = indexedSqlCache.get(method);
-
- if (indexedSql == null) {
-
- // index the sql and method args
- indexedSql = indexSql(method, sql);
-
- // cache the indexed sql for re-use
- indexedSqlCache.put(method, indexedSql);
- }
-
- final PreparedSql preparedSql = indexedSql.prepareSql(db, methodArgs);
- return preparedSql;
- }
-
- /**
- * Indexes an sql statement and method args based on the supplied
- * method and it's arguments.
- *
- * @param method
- * @param sql
- * @return an indexed sql statement and arguments
- */
- private IndexedSql indexSql(Method method, String sql) {
-
- Map<String, IndexedArgument> parameterIndex = buildParameterIndex(method);
-
- // build a regex to extract parameter names from the sql statement
- StringBuilder sb = new StringBuilder();
- sb.append(bindingDelimiter);
- sb.append("{1}(\\?");
- for (String name : parameterIndex.keySet()) {
- sb.append("|");
- // strip binding delimeter from name
- sb.append(name);
- }
- sb.append(')');
-
- // identify parameters, replace with the '?' PreparedStatement
- // delimiter and build the PreparedStatement parameters array
- final String regex = sb.toString();
- final Pattern p = Pattern.compile(regex);
- final Matcher m = p.matcher(sql);
- final StringBuffer buffer = new StringBuffer();
-
- List<IndexedArgument> indexedArgs = Utils.newArrayList();
- int count = 0;
- while (m.find()) {
- String binding = m.group(1);
- m.appendReplacement(buffer, "?");
-
- IndexedArgument indexedArg;
- if ("?".equals(binding)) {
- // standard ? JDBC placeholder
- indexedArg = parameterIndex.get("arg" + count);
- } else {
- // named placeholder
- indexedArg = parameterIndex.get(binding);
- }
-
- if (indexedArg == null) {
- throw new IciqlException("Unbound SQL parameter '{0}' in {1}.{2}",
- binding, method.getDeclaringClass().getSimpleName(), method.getName());
- }
- indexedArgs.add(indexedArg);
-
- count++;
- }
- m.appendTail(buffer);
-
- final String statement = buffer.toString();
-
- // create an IndexedSql container for the statement and indexes
- return new IndexedSql(statement, Collections.unmodifiableList(indexedArgs));
-
- }
-
- /**
- * Builds an index of parameter name->(position,typeAdapter) from the method arguments
- * array. This index is calculated once per method.
- *
- * @param method
- * @return a bindings map of ("name", IndexedArgument) pairs
- */
- private Map<String, IndexedArgument> buildParameterIndex(Method method) {
-
- Map<String, IndexedArgument> index = new TreeMap<String, IndexedArgument>();
-
- Annotation [][] annotationsMatrix = method.getParameterAnnotations();
- for (int i = 0; i < annotationsMatrix.length; i++) {
-
- Annotation [] annotations = annotationsMatrix[i];
-
- /*
- * Conditionally map the bean properties of the method argument
- * class to Method and Field instances.
- */
- BindBean bean = getAnnotation(BindBean.class, annotations);
- if (bean != null) {
- final String prefix = bean.value();
- final Class<?> argumentClass = method.getParameterTypes()[i];
- Map<String, IndexedArgument> beanIndex = buildBeanIndex(i, prefix, argumentClass);
- index.putAll(beanIndex);
- }
-
- Class<? extends DataTypeAdapter<?>> typeAdapter = Utils.getDataTypeAdapter(annotations);
- final IndexedArgument indexedArgument = new IndexedArgument(i, typeAdapter);
-
- // :N - 1-indexed, like JDBC ResultSet
- index.put("" + (i + 1), indexedArgument);
-
- // argN - 0-indexed, like Reflection
- index.put("arg" + i, indexedArgument);
-
- // Bound name
- Bind binding = getAnnotation(Bind.class, annotations);
- if (binding!= null && !binding.value().isEmpty()) {
- index.put(binding.value(), indexedArgument);
- }
-
- // try mapping Java 8 argument names, may overwrite argN
- try {
- Class<?> nullArgs = null;
- Method getParameters = method.getClass().getMethod("getParameters", nullArgs);
- if (getParameters != null) {
- Object [] parameters = (Object []) getParameters.invoke(method, nullArgs);
- if (parameters != null) {
- Object o = parameters[i];
- Method getName = o.getClass().getMethod("getName", nullArgs);
- String j8name = getName.invoke(o, nullArgs).toString();
- if (!j8name.isEmpty()) {
- index.put(j8name, indexedArgument);
- }
- }
- }
- } catch (Throwable t) {
- }
- }
-
- return index;
- }
-
- /**
- * Builds an index of parameter name->(position,method) from the method arguments
- * array. This index is calculated once per method.
- *
- * @param argumentIndex
- * @param prefix
- * @param beanClass
- * @return a bindings map of ("prefix.property", IndexedArgument) pairs
- */
- private Map<String, IndexedArgument> buildBeanIndex(int argumentIndex, String prefix, Class<?> beanClass) {
-
- final String beanPrefix = StringUtils.isNullOrEmpty(prefix) ? "" : (prefix + ".");
- final Map<String, IndexedArgument> index = new TreeMap<String, IndexedArgument>();
-
- // map JavaBean property getters
- for (Method method : beanClass.getMethods()) {
-
- if (Modifier.isStatic(method.getModifiers())
- || method.getReturnType() == void.class
- || method.getParameterTypes().length > 0
- || method.getDeclaringClass() == Object.class) {
-
- // not a JavaBean property
- continue;
- }
-
- final String propertyName;
- final String name = method.getName();
- if (name.startsWith("get")) {
- propertyName = method.getName().substring(3);
- } else if (name.startsWith("is")) {
- propertyName = method.getName().substring(2);
- } else {
- propertyName = null;
- }
-
- if (propertyName == null) {
- // not a conventional JavaBean property
- continue;
- }
-
- final String binding = beanPrefix + preparePropertyName(propertyName);
- final IndexedArgument indexedArg = new IndexedArgument(argumentIndex, method);
-
- index.put(binding, indexedArg);
- }
-
- // map public instance fields
- for (Field field : beanClass.getFields()) {
-
- if (Modifier.isStatic(field.getModifiers())) {
- // not a JavaBean property
- continue;
- }
-
- final String binding = beanPrefix + preparePropertyName(field.getName());
- final IndexedArgument indexedArg = new IndexedArgument(argumentIndex, field);
-
- index.put(binding, indexedArg);
-
- }
-
- return index;
- }
-
- @SuppressWarnings("unchecked")
- private <T> T getAnnotation(Class<T> annotationClass, Annotation [] annotations) {
- if (annotations != null) {
- for (Annotation annotation : annotations) {
- if (annotation.annotationType() == annotationClass) {
- return (T) annotation;
- }
- }
- }
- return null;
- }
-
- private String preparePropertyName(String value) {
- return Character.toLowerCase(value.charAt(0)) + value.substring(1);
- }
-
- /*
- *
- * Standard Dao method implementations delegate to the underlying Db
- *
- */
-
- @Override
- public final Db db() {
- return db;
- }
-
- @Override
- public final <T> boolean insert(T t) {
- return db.insert(t);
- }
-
- @Override
- public final <T> void insertAll(List<T> t) {
- db.insertAll(t);
- }
-
- @Override
- public final <T> long insertAndGetKey(T t) {
- return db.insertAndGetKey(t);
- }
-
- @Override
- public final <T> List<Long> insertAllAndGetKeys(List<T> t) {
- return db.insertAllAndGetKeys(t);
- }
-
- @Override
- public final <T> boolean update(T t) {
- return db.update(t);
- }
-
- @Override
- public final <T> void updateAll(List<T> t) {
- db.updateAll(t);
- }
-
- @Override
- public final <T> void merge(T t) {
- db.merge(t);
- }
-
- @Override
- public final <T> boolean delete(T t) {
- return db.delete(t);
- }
-
- @Override
- public final <T> void deleteAll(List<T> t) {
- db.deleteAll(t);
- }
-
- @Override
- public final void close() {
- db.close();
- }
-
- /**
- * Container class to hold the prepared JDBC SQL statement and execution
- * parameters.
- */
- private class PreparedSql {
- final String sql;
- final Object [] parameters;
-
- PreparedSql(String sql, Object [] parameters) {
- this.sql = sql;
- this.parameters = parameters;
- }
-
- @Override
- public String toString() {
- return sql;
- }
-
- }
-
- /**
- * Container class to hold a parsed JDBC SQL statement and
- * IndexedParameters.
- * <p>
- * Instances of this class are cached because they are functional processing
- * containers as they contain Method and Field references for binding beans
- * and matching to method arguments.
- * </p>
- */
- private class IndexedSql {
- final String sql;
- final List<IndexedArgument> indexedArgs;
-
- IndexedSql(String sql, List<IndexedArgument> indexedArgs) {
- this.sql = sql;
- this.indexedArgs = indexedArgs;
- }
-
- /**
- * Prepares the method arguments for statement execution.
- *
- * @param db
- * @param methodArgs
- * @return the prepared sql statement and parameters
- */
- PreparedSql prepareSql(Db db, Object [] methodArgs) {
-
- Object [] parameters = new Object[indexedArgs.size()];
-
- for (int i = 0; i < indexedArgs.size(); i++) {
-
- IndexedArgument indexedArg = indexedArgs.get(i);
- Object methodArg = methodArgs[indexedArg.index];
-
- Object value = methodArg;
- Class<? extends DataTypeAdapter<?>> typeAdapter = indexedArg.typeAdapter;
-
- if (indexedArg.method != null) {
-
- // execute the bean method
- try {
-
- value = indexedArg.method.invoke(methodArg);
- typeAdapter = Utils.getDataTypeAdapter(indexedArg.method.getAnnotations());
-
- } catch (Exception e) {
- throw new IciqlException(e);
- }
-
- } else if (indexedArg.field != null) {
-
- // extract the field value
- try {
-
- value = indexedArg.field.get(methodArg);
- typeAdapter = Utils.getDataTypeAdapter(indexedArg.field.getAnnotations());
-
- } catch (Exception e) {
- throw new IciqlException(e);
- }
-
- } else if (typeAdapter == null) {
-
- // identify the type adapter for the argument class
- typeAdapter = Utils.getDataTypeAdapter(methodArg.getClass().getAnnotations());
- }
-
- // prepare the parameter
- parameters[i] = db.getDialect().serialize(value, typeAdapter);
-
- }
-
- return new PreparedSql(sql, parameters);
-
- }
-
- @Override
- public String toString() {
- return sql;
- }
- }
-
- /**
- * IndexedArgument holds cached information about how to process an method
- * argument by it's index in the method arguments array.
- * <p>
- * An argument may be passed-through, might be bound to a bean property,
- * might be transformed with a type adapter, or a combination of these.
- * </p>
- */
- private class IndexedArgument {
- final int index;
- final Class<? extends DataTypeAdapter<?>> typeAdapter;
- final Method method;
- final Field field;
-
- IndexedArgument(int index, Class<? extends DataTypeAdapter<?>> typeAdapter) {
- this.index = index;
- this.typeAdapter = typeAdapter;
- this.method = null;
- this.field = null;
- }
-
- IndexedArgument(int methodArgIndex, Method method) {
- this.index = methodArgIndex;
- this.typeAdapter = null;
- this.method = method;
- this.field = null;
- }
-
- IndexedArgument(int methodArgIndex, Field field) {
- this.index = methodArgIndex;
- this.typeAdapter = null;
- this.method = null;
- this.field = field;
- }
-
- @Override
- public String toString() {
-
- String accessor;
- if (method != null) {
- accessor = "M:" + method.getDeclaringClass().getSimpleName() + "." + method.getName();
- } else if (field != null) {
- accessor = "F:" + field.getDeclaringClass().getSimpleName() + "." + field.getName();
- } else {
- accessor = "A:arg";
- }
-
- return index + ":" + accessor + (typeAdapter == null ? "" : (":" + typeAdapter.getSimpleName()));
- }
-
- }
-
- }
|