diff options
author | James Moger <james.moger@gitblit.com> | 2014-11-06 15:34:50 -0500 |
---|---|---|
committer | James Moger <james.moger@gitblit.com> | 2014-11-09 11:15:14 -0500 |
commit | 96d0aca9ff3b29be62bc6558af80fe115b646b88 (patch) | |
tree | 250e525f8975d44c95c5111bfb66d6d2cdd84919 /src/main/java/com/iciql/DaoProxy.java | |
parent | db0d58c22a0bd4fa2baf023428599757aa4db381 (diff) | |
download | iciql-96d0aca9ff3b29be62bc6558af80fe115b646b88.tar.gz iciql-96d0aca9ff3b29be62bc6558af80fe115b646b88.zip |
Implement Dao proxy generation with annotated sql statement execution
This functionality is inspired by JDBI but is not based on it's implementation.
Diffstat (limited to 'src/main/java/com/iciql/DaoProxy.java')
-rw-r--r-- | src/main/java/com/iciql/DaoProxy.java | 792 |
1 files changed, 792 insertions, 0 deletions
diff --git a/src/main/java/com/iciql/DaoProxy.java b/src/main/java/com/iciql/DaoProxy.java new file mode 100644 index 0000000..db5f911 --- /dev/null +++ b/src/main/java/com/iciql/DaoProxy.java @@ -0,0 +1,792 @@ +/* + * 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.Iciql.TypeAdapter; +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 buildProxy() { + + if (!daoInterface.isInterface()) { + throw new IciqlException("Dao {0} must be an interface!", daoInterface.getName()); + } + + ClassLoader classLoader = db.getClass().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(); + return executeQuery(method, args, sql); + + } else if (method.isAnnotationPresent(SqlStatement.class)) { + + String sql = method.getAnnotation(SqlStatement.class).value(); + return executeStatement(method, args, sql); + + } 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); + + // determine the return type adapter, if any + DataTypeAdapter<?> adapter = null; + for (Annotation annotation : method.getAnnotations()) { + if (annotation.annotationType() == TypeAdapter.class) { + TypeAdapter typeAdapter = (TypeAdapter) annotation; + adapter = db.getDialect().getAdapter(typeAdapter.value()); + break; + } else if (annotation.annotationType().isAnnotationPresent(TypeAdapter.class)) { + TypeAdapter typeAdapter = annotation.annotationType().getAnnotation(TypeAdapter.class); + adapter = db.getDialect().getAdapter(typeAdapter.value()); + break; + } + } + + /* + * 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 o = rs.getObject(1); + Object value; + + if (adapter == null) { + // use internal Iciql type conversion + value = Utils.convert(o, returnType); + } else { + // use the type adapter to convert the JDBC object to a domain type + value = adapter.deserialize(o); + } + + objects.add(value); + + } + + } 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] = prepareParameter(db, value, typeAdapter); + + } + + return new PreparedSql(sql, parameters); + + } + + /** + * Prepares a method argument to an sql parameter for transmission to a + * database. + * + * @param db + * @param arg + * @param typeAdapter + * @return a prepared parameter + */ + private Object prepareParameter(Db db, Object arg, Class<? extends DataTypeAdapter<?>> typeAdapter) { + + if (typeAdapter != null) { + + // use a type adapter to serialize the method argument + Object o = db.getDialect().serialize(arg, typeAdapter); + return o; + + } else { + + // use the method argument + return arg; + + } + + } + + @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())); + } + + } + +} |