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 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 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 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;