diff --git a/common/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapper.java b/common/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapper.java
index 5c11af2..128d57e 100644
--- a/common/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapper.java
+++ b/common/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapper.java
@@ -35,6 +35,14 @@ public interface JdbcMapper extends Closeable {
* @return
*/
Class extends SQLParser> sqlParser() default SQLParser.class;
+
+ /**
+ * This defaults to SimpleSQLParser, PrestoSQLParser is another option for Java 8, or implement your own
+ * @return
+ */
+ DatabaseType databaseType() default DatabaseType.DEFAULT;
+ String arrayNumberTypeName() default "";
+ String arrayStringTypeName() default "";
}
@Retention(RetentionPolicy.SOURCE)
@@ -109,4 +117,24 @@ public interface JdbcMapper extends Closeable {
return def;
}
}
+
+ public enum DatabaseType {
+ DEFAULT(null, null),
+ STANDARD("number", "text"),
+ ORACLE("ARRAY_NUM_TYPE", "ARRAY_STR_TYPE");
+
+ public final String arrayNumberTypeName, arrayStringTypeName;
+
+ private DatabaseType(final String arrayNumberTypeName, final String arrayStringTypeName) {
+ this.arrayNumberTypeName = arrayNumberTypeName;
+ this.arrayStringTypeName = arrayStringTypeName;
+ }
+
+ public DatabaseType nonDefault(final DatabaseType def) {
+ if(this != DEFAULT)
+ return this;
+ //return def == DEFAULT ? STANDARD : def;
+ return def; // we guarantee this to be not DEFAULT in JdbcMapperProcessor
+ }
+ }
}
diff --git a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/InListVariableElement.java b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/InListVariableElement.java
new file mode 100644
index 0000000..3fdf53f
--- /dev/null
+++ b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/InListVariableElement.java
@@ -0,0 +1,79 @@
+package com.moparisthebest.jdbc.codegen;
+
+import javax.lang.model.element.*;
+import javax.lang.model.type.TypeMirror;
+import java.lang.annotation.Annotation;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Created by mopar on 6/1/17.
+ */
+class InListVariableElement implements VariableElement {
+
+ final VariableElement delegate;
+
+ InListVariableElement(final VariableElement delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Object getConstantValue() {
+ return delegate.getConstantValue();
+ }
+
+ @Override
+ public TypeMirror asType() {
+ return delegate.asType();
+ }
+
+ @Override
+ public ElementKind getKind() {
+ return delegate.getKind();
+ }
+
+ @Override
+ public List extends AnnotationMirror> getAnnotationMirrors() {
+ return delegate.getAnnotationMirrors();
+ }
+
+ @Override
+ public A getAnnotation(final Class annotationType) {
+ return delegate.getAnnotation(annotationType);
+ }
+
+ @Override
+ public Set getModifiers() {
+ return delegate.getModifiers();
+ }
+
+ @Override
+ public Name getSimpleName() {
+ return delegate.getSimpleName();
+ }
+
+ @Override
+ public Element getEnclosingElement() {
+ return delegate.getEnclosingElement();
+ }
+
+ @Override
+ public List extends Element> getEnclosedElements() {
+ return delegate.getEnclosedElements();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ return delegate.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return delegate.hashCode();
+ }
+
+ @Override
+ public R accept(final ElementVisitor v, final P p) {
+ return delegate.accept(v, p);
+ }
+}
diff --git a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java
index 42fb7fc..75bf63f 100644
--- a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java
+++ b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java
@@ -3,9 +3,7 @@ package com.moparisthebest.jdbc.codegen;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
-import javax.lang.model.type.MirroredTypeException;
-import javax.lang.model.type.TypeKind;
-import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.*;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
@@ -23,16 +21,19 @@ import static com.moparisthebest.jdbc.TryClose.tryClose;
* Created by mopar on 5/24/17.
*/
@SupportedAnnotationTypes("com.moparisthebest.jdbc.codegen.JdbcMapper.Mapper")
+@SupportedOptions({"jdbcMapper.databaseType", "jdbcMapper.arrayNumberTypeName", "jdbcMapper.arrayStringTypeName"})
@SupportedSourceVersion(SourceVersion.RELEASE_5)
public class JdbcMapperProcessor extends AbstractProcessor {
- private static final Pattern paramPattern = Pattern.compile("\\{([^}]+)\\}");
+ private static final Pattern paramPattern = Pattern.compile("\\{(([^\\s]+)\\s+(([Nn][Oo][Tt]\\s+)?[Ii][Nn]\\s+))?([^}]+)\\}");
private static final CompileTimeResultSetMapper rsm = new CompileTimeResultSetMapper();
private Types types;
private TypeMirror sqlExceptionType, stringType, numberType, utilDateType, readerType, clobType,
- byteArrayType, inputStreamType, fileType, blobType, sqlArrayType
+ byteArrayType, inputStreamType, fileType, blobType, sqlArrayType, collectionType
;
+ private JdbcMapper.DatabaseType defaultDatabaseType;
+ private String defaultArrayNumberTypeName, defaultArrayStringTypeName;
public JdbcMapperProcessor() {
//out.println("JdbcMapperProcessor running!");
@@ -58,6 +59,15 @@ public class JdbcMapperProcessor extends AbstractProcessor {
//byteArrayType = elements.getTypeElement(byte.class.getCanonicalName()).asType();
byteArrayType = types.getArrayType(types.getPrimitiveType(TypeKind.BYTE));
sqlArrayType = elements.getTypeElement(java.sql.Array.class.getCanonicalName()).asType();
+ collectionType = types.getDeclaredType(elements.getTypeElement(Collection.class.getCanonicalName()), types.getWildcardType(null, null));
+ final String databaseType = processingEnv.getOptions().get("JdbcMapper.databaseType");
+ defaultDatabaseType = databaseType == null ? JdbcMapper.DatabaseType.STANDARD : JdbcMapper.DatabaseType.valueOf(databaseType);
+ defaultArrayNumberTypeName = processingEnv.getOptions().get("JdbcMapper.arrayNumberTypeName");
+ if(defaultArrayNumberTypeName == null || defaultArrayNumberTypeName.isEmpty())
+ defaultArrayNumberTypeName = defaultDatabaseType.arrayNumberTypeName;
+ defaultArrayStringTypeName = processingEnv.getOptions().get("JdbcMapper.arrayStringTypeName");
+ if(defaultArrayStringTypeName == null || defaultArrayStringTypeName.isEmpty())
+ defaultArrayStringTypeName = defaultDatabaseType.arrayStringTypeName;
}
@Override
@@ -78,6 +88,9 @@ public class JdbcMapperProcessor extends AbstractProcessor {
}
final TypeElement genClass = (TypeElement) element;
final JdbcMapper.Mapper mapper = genClass.getAnnotation(JdbcMapper.Mapper.class);
+ final JdbcMapper.DatabaseType databaseType = mapper.databaseType().nonDefault(defaultDatabaseType);
+ final String arrayNumberTypeName = !mapper.arrayNumberTypeName().isEmpty() ? mapper.arrayNumberTypeName() : defaultArrayNumberTypeName;
+ final String arrayStringTypeName = !mapper.arrayStringTypeName().isEmpty() ? mapper.arrayStringTypeName() : defaultArrayStringTypeName;
final String sqlParserMirror = getSqlParser(mapper).toString();
//final SQLParser parser = new SimpleSQLParser();//(SQLParser)Class.forName(mapper.sqlParser().getCanonicalName()).newInstance();
//final SQLParser parser = mapper.sqlParser().equals(SQLParser.class) ? new SimpleSQLParser() : mapper.sqlParser().newInstance();
@@ -223,14 +236,37 @@ public class JdbcMapperProcessor extends AbstractProcessor {
final Matcher bindParamMatcher = paramPattern.matcher(sql.value());
final StringBuffer sb = new StringBuffer();
while (bindParamMatcher.find()) {
- final String paramName = bindParamMatcher.group(1);
+ final String paramName = bindParamMatcher.group(5);
final VariableElement bindParam = paramMap.get(paramName);
if (bindParam == null) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, String.format("@JdbcMapper.SQL sql has bind param '%s' not in method parameter list", paramName), methodElement);
continue;
}
- bindParams.add(bindParam);
- bindParamMatcher.appendReplacement(sb, "?");
+ final String inColumnName = bindParamMatcher.group(2);
+ if(inColumnName == null) {
+ bindParams.add(bindParam);
+ bindParamMatcher.appendReplacement(sb, "?");
+ } else {
+ bindParams.add(new InListVariableElement(bindParam));
+ final boolean not = bindParamMatcher.group(4) != null;
+ final String replacement;
+ switch (databaseType) {
+ case ORACLE:
+ replacement = not ?
+ "(" + inColumnName + " NOT IN(select column_value from table(?)))" :
+ "(" + inColumnName + " IN(select column_value from table(?)))";
+ break;
+ case STANDARD:
+ replacement = not ?
+ "(" + inColumnName + " != ANY(?))" :
+ "(" + inColumnName + " = ANY(?))";
+ break;
+ default:
+ processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "default DatabaseType? should never happen!!", bindParam);
+ return false;
+ }
+ bindParamMatcher.appendReplacement(sb, replacement);
+ }
}
bindParamMatcher.appendTail(sb);
sqlStatement = sb.toString();
@@ -258,7 +294,7 @@ public class JdbcMapperProcessor extends AbstractProcessor {
// now bind parameters
int count = 0;
for (final VariableElement param : bindParams)
- setObject(w, ++count, param);
+ setObject(w, ++count, databaseType, arrayNumberTypeName, arrayStringTypeName, param);
if (!parsedSQl.isSelect()) {
if (returnType.equals("void")) {
@@ -347,13 +383,74 @@ public class JdbcMapperProcessor extends AbstractProcessor {
return true;
}
- private void setObject(final Writer w, final int index, final VariableElement param) throws SQLException, IOException {
+ private void setObject(final Writer w, final int index, final JdbcMapper.DatabaseType databaseType, final String arrayNumberTypeName, final String arrayStringTypeName, final VariableElement param) throws SQLException, IOException {
String variableName = param.getSimpleName().toString();
final TypeMirror o = param.asType();
w.write("\t\t\t");
String method = null;
// special behavior
+ if(param instanceof InListVariableElement) {
+ final boolean collection;
+ final TypeMirror componentType;
+ if(o.getKind() == TypeKind.ARRAY) {
+ collection = false;
+ componentType = ((ArrayType)o).getComponentType();
+ } else if(o.getKind() == TypeKind.DECLARED && types.isAssignable(o, collectionType)) {
+ collection = true;
+ final DeclaredType dt = (DeclaredType)o;
+ if(dt.getTypeArguments().isEmpty()) {
+ processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL in list syntax requires a Collection with a generic type parameter" +o.toString(), ((InListVariableElement)param).delegate);
+ return;
+ }
+ componentType = dt.getTypeArguments().get(0);
+ } else {
+ processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL in list syntax only valid on Collections or arrays" +o.toString(), ((InListVariableElement)param).delegate);
+ return;
+ }
+ w.write("ps.setArray(");
+ w.write(Integer.toString(index));
+ w.write(", ");
+ final String type = types.isAssignable(componentType, numberType) ? arrayNumberTypeName : arrayStringTypeName;
+ switch (databaseType) {
+ case ORACLE:
+ w.write("conn.unwrap(oracle.jdbc.OracleConnection.class).createOracleArray(\"");
+
+ // todo: if oracle driver is not on compile-time classpath, would need to do:
+ // we could also create a fake-oracle module that just had a oracle.jdbc.OracleConnection class implementing createOracleArray()...
+ /*
+ private static final Class> oracleConnection;
+ private static final Method createArray;
+
+ static {
+ Class> oc;
+ Method ca;
+ try {
+ oc = Class.forName("oracle.jdbc.OracleConnection");
+ ca = oc.getDeclaredMethod("createOracleArray", String.class, Object.class);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ oracleConnection = oc;
+ createArray = ca;
+ }
+ */
+ //w.write("(Array) createArray.invoke(conn.unwrap(oracleConnection), \"");
+ break;
+ case STANDARD:
+ w.write("conn.createArrayOf(\"");
+ break;
+ default:
+ processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "default DatabaseType? should never happen!!", ((InListVariableElement)param).delegate);
+ }
+ w.write(type);
+ w.write("\", ");
+ w.write(variableName);
+ if(collection)
+ w.write(".toArray()");
+ w.write("));\n");
+ return;
+ }
final JdbcMapper.Blob blob = param.getAnnotation(JdbcMapper.Blob.class);
if(blob != null) {
if (types.isAssignable(o, stringType)) {
@@ -457,6 +554,37 @@ public class JdbcMapperProcessor extends AbstractProcessor {
return float.class;
case DOUBLE:
return double.class;
+ case ARRAY:
+ // yuck
+ final TypeMirror arrayComponentType = ((ArrayType)tm).getComponentType();
+ switch (arrayComponentType.getKind()) {
+ case BOOLEAN:
+ return boolean[].class;
+ case BYTE:
+ return byte[].class;
+ case SHORT:
+ return short[].class;
+ case INT:
+ return int[].class;
+ case LONG:
+ return long[].class;
+ case CHAR:
+ return char[].class;
+ case FLOAT:
+ return float[].class;
+ case DOUBLE:
+ return double[].class;
+ case ARRAY:
+ throw new RuntimeException("multi-dimensional arrays are not supported");
+ default:
+ return Class.forName("[L" + arrayComponentType.toString() + ";");
+ }
+ case DECLARED:
+ if(!((DeclaredType)tm).getTypeArguments().isEmpty()) {
+ final String classWithGenerics = tm.toString();
+ return Class.forName(classWithGenerics.substring(0, classWithGenerics.indexOf('<')));
+ }
+ // fallthrough otherwise...
default:
return Class.forName(tm.toString());
}
diff --git a/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAO.java b/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAO.java
index 928a354..a9775e7 100644
--- a/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAO.java
+++ b/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAO.java
@@ -2,7 +2,6 @@ package com.moparisthebest.jdbc.codegen;
import com.moparisthebest.jdbc.dto.FieldPerson;
-import java.io.Closeable;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.List;
@@ -14,6 +13,7 @@ import java.util.Map;
*/
@JdbcMapper.Mapper(
// jndiName = "bob",
+// databaseType = JdbcMapper.DatabaseType.ORACLE
// cachePreparedStatements = false
// , sqlParser = SimpleSQLParser.class
)
@@ -40,6 +40,12 @@ public interface PersonDAO {
@JdbcMapper.SQL("SELECT first_name, last_name FROM person WHERE last_name = {lastName}")
List getPeople(String lastName) throws SQLException;
+ @JdbcMapper.SQL("SELECT first_name, last_name FROM person WHERE {last_name not in lastName}")
+ List getPeople(String[] lastName) throws SQLException;
+
+ @JdbcMapper.SQL("SELECT first_name, last_name FROM person WHERE {last_name in lastName}")
+ List getPeople(List lastName) throws SQLException;
+
@JdbcMapper.SQL("SELECT first_name, last_name FROM person WHERE last_name = {lastName}")
FieldPerson[] getPeopleArray(String lastName) throws SQLException;