aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/com/iciql
diff options
context:
space:
mode:
authorJames Moger <james.moger@gitblit.com>2014-11-06 15:34:50 -0500
committerJames Moger <james.moger@gitblit.com>2014-11-09 11:15:14 -0500
commit96d0aca9ff3b29be62bc6558af80fe115b646b88 (patch)
tree250e525f8975d44c95c5111bfb66d6d2cdd84919 /src/main/java/com/iciql
parentdb0d58c22a0bd4fa2baf023428599757aa4db381 (diff)
downloadiciql-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')
-rw-r--r--src/main/java/com/iciql/Dao.java162
-rw-r--r--src/main/java/com/iciql/DaoProxy.java792
-rw-r--r--src/main/java/com/iciql/Db.java18
3 files changed, 968 insertions, 4 deletions
diff --git a/src/main/java/com/iciql/Dao.java b/src/main/java/com/iciql/Dao.java
new file mode 100644
index 0000000..29b42f0
--- /dev/null
+++ b/src/main/java/com/iciql/Dao.java
@@ -0,0 +1,162 @@
+/*
+ * 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.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+
+/**
+ * The Dao interface defines all CRUD methods for handling SQL object operations.
+ *
+ * @author James Moger
+ *
+ */
+public interface Dao extends AutoCloseable {
+
+ /**
+ * Insert an object into the database.
+ *
+ * @param t
+ * @return true if successful
+ */
+ <T> boolean insert(T t);
+
+ /**
+ * Insert an object into the database and return it's primary key.
+ *
+ * @param t
+ * @return
+ */
+ <T> long insertAndGetKey(T t);
+
+ /**
+ * Insert all objects into the database.
+ *
+ * @param list
+ */
+ <T> void insertAll(List<T> list);
+
+ /**
+ * Insert all objects into the database and return the list of primary keys.
+ *
+ * @param t
+ * @return a list of primary keys
+ */
+ <T> List<Long> insertAllAndGetKeys(List<T> t);
+
+ /**
+ * Updates an object in the database.
+ *
+ * @param t
+ * @return true if successful
+ */
+ <T> boolean update(T t);
+
+ /**
+ * Updates all objects in the database.
+ *
+ * @param list
+ */
+ <T> void updateAll(List<T> list);
+
+ /**
+ * Inserts or updates an object in the database.
+ *
+ * @param t
+ */
+ <T> void merge(T t);
+
+ /**
+ * Deletes an object from the database.
+ *
+ * @param t
+ * @return true if successful
+ */
+ <T> boolean delete(T t);
+
+ /**
+ * Deletes all objects from the database.
+ *
+ * @param list
+ */
+ <T> void deleteAll(List<T> list);
+
+ /**
+ * Returns the underlying Db instance for lower-level access to database methods
+ * or direct JDBC access.
+ *
+ * @return the db instance
+ */
+ Db db();
+
+ /**
+ * Close the underlying Db instance.
+ */
+ @Override
+ void close();
+
+ /**
+ * Used to specify custom names for method parameters to be used
+ * for the SqlQuery or SqlUpdate annotations.
+ *
+ * You don't need to explicitly bind the parameters as each parameter
+ * is accessible by the standard "argN" syntax (0-indexed).
+ *
+ * Additionally, if you are compiling with Java 8 AND specifying the
+ * -parameters flag for javac, then you may use the parameter's name.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ ElementType.PARAMETER })
+ public @interface Bind {
+ String value();
+ }
+
+ /**
+ *
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ ElementType.PARAMETER })
+ public @interface BindBean {
+ String value() default "";
+ }
+
+ /**
+ * Used to indicate that a method should execute a query.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ ElementType.METHOD })
+ public @interface SqlQuery {
+ String value();
+ }
+
+ /**
+ * Used to indicate that a method should execute a statement.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ ElementType.METHOD })
+ public @interface SqlStatement {
+ String value();
+ }
+
+ public class BeanBinder {
+ public void bind(BindBean bind, Object obj) {
+
+ }
+ }
+}
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()));
+ }
+
+ }
+
+}
diff --git a/src/main/java/com/iciql/Db.java b/src/main/java/com/iciql/Db.java
index ca43e63..13c9260 100644
--- a/src/main/java/com/iciql/Db.java
+++ b/src/main/java/com/iciql/Db.java
@@ -194,7 +194,17 @@ public class Db implements AutoCloseable {
return new Db(conn);
}
-
+ /**
+ * Returns a new DAO instance for the specified class.
+ *
+ * @param daoClass
+ * @return
+ * @throws Exception
+ */
+ @SuppressWarnings("resource")
+ public <X extends Dao> X open(Class<X> daoClass) {
+ return new DaoProxy<X>(this, daoClass).buildProxy();
+ }
/**
* Convenience function to avoid import statements in application code.
@@ -675,7 +685,7 @@ public class Db implements AutoCloseable {
*/
public ResultSet executeQuery(String sql, Object... args) {
try {
- if (args.length == 0) {
+ if (args == null || args.length == 0) {
return conn.createStatement().executeQuery(sql);
} else {
PreparedStatement stat = conn.prepareStatement(sql);
@@ -717,7 +727,7 @@ public class Db implements AutoCloseable {
public <T> List<T> executeQuery(Class<? extends T> modelClass, String sql, Object... args) {
ResultSet rs = null;
try {
- if (args.length == 0) {
+ if (args == null || args.length == 0) {
rs = conn.createStatement().executeQuery(sql);
} else {
PreparedStatement stat = conn.prepareStatement(sql);
@@ -748,7 +758,7 @@ public class Db implements AutoCloseable {
Statement stat = null;
try {
int updateCount;
- if (args.length == 0) {
+ if (args == null || args.length == 0) {
stat = conn.createStatement();
updateCount = stat.executeUpdate(sql);
} else {