From edd6fc6e6520cf6cb7b2402a813ef2155da834f2 Mon Sep 17 00:00:00 2001 From: moparisthebest Date: Wed, 14 Mar 2018 02:25:11 -0400 Subject: [PATCH] Remove Blob and Clob annotations in favor of SQL bind syntax --- .../jdbc/codegen/JdbcMapper.java | 29 --- .../jdbc/codegen/JdbcMapperProcessor.java | 168 ++++++++++-------- .../jdbc/codegen/SimpleSQLChecker.java | 54 +++--- ...ement.java => SpecialVariableElement.java} | 25 ++- .../jdbc/codegen/ParamPatternTest.java | 39 ++++ .../jdbc/codegen/PersonDAO.java | 4 +- .../jdbc/codegen/PrestoPersonDAO.java | 4 +- 7 files changed, 187 insertions(+), 136 deletions(-) rename jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/{InListVariableElement.java => SpecialVariableElement.java} (72%) create mode 100644 jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/ParamPatternTest.java 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 d959bf4..d8b87a4 100644 --- a/common/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapper.java +++ b/common/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapper.java @@ -103,35 +103,6 @@ public interface JdbcMapper extends Closeable { long maxRows() default -1; } - // these are to annotate parameters for special bind to PreparedStatement behavior - - /** - * Use PreparedStatement.setBlob - * - * Only required for String - * - * Also valid for byte[], InputStream, Blob, File. But for those .setBlob is called even without this annotation - */ - @Retention(RetentionPolicy.SOURCE) - @Target({ElementType.PARAMETER}) - public @interface Blob { - /** - * The charsetName sent into String.getBytes() - */ - String charsetName() default "UTF-8"; - } - - /** - * Use PreparedStatement.setClob - * - * Only valid for String - * - * Also valid for Clob, Reader. But for those .setClob is called even without this annotation - */ - @Retention(RetentionPolicy.SOURCE) - @Target({ElementType.PARAMETER}) - public @interface Clob {} - /** * Equivalent to QueryMapper.toObject for arrays, and .toSingleMap for maps */ 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 701747f..336be7c 100644 --- a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java +++ b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java @@ -34,7 +34,7 @@ import static com.moparisthebest.jdbc.codegen.JdbcMapperFactory.SUFFIX; @SupportedSourceVersion(SourceVersion.RELEASE_5) public class JdbcMapperProcessor extends AbstractProcessor { - private static final Pattern paramPattern = Pattern.compile("\\{(([^\\s]+)\\s+(([Nn][Oo][Tt]\\s+)?[Ii][Nn]\\s+))?([^}]+)\\}"); + public static final Pattern paramPattern = Pattern.compile("\\{(([^\\s]+)\\s+(([Nn][Oo][Tt]\\s+)?[Ii][Nn]\\s+))?([BbCc][Ll][Oo][Bb]\\s*:\\s*([^:]+\\s*:\\s*)?)?([^}]+)\\}"); public static final SourceVersion RELEASE_8; public static boolean java8; @@ -334,19 +334,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(5); + final String paramName = bindParamMatcher.group(7); 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; } unusedParams.remove(paramName); + final String clobBlob = bindParamMatcher.group(5); final String inColumnName = bindParamMatcher.group(2); if (inColumnName == null) { - bindParams.add(bindParam); bindParamMatcher.appendReplacement(sb, "?"); + if(clobBlob == null){ + bindParams.add(bindParam); + } else { + // regex ensures this can only be C for clob or B for blob + final boolean clobNotBlob = 'C' == Character.toUpperCase(clobBlob.charAt(0)); + final String blobCharset = bindParamMatcher.group(6); + if(clobNotBlob) { + if(blobCharset != null) + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "blob character set not valid with clob", bindParam); + bindParams.add(new SpecialVariableElement(bindParam, SpecialVariableElement.SpecialType.CLOB)); + } else { + bindParams.add(new SpecialVariableElement(bindParam, SpecialVariableElement.SpecialType.BLOB, blobCharset == null ? null : + blobCharset.substring(0, blobCharset.indexOf(':')).trim() + )); + } + } } else { - bindParams.add(new InListVariableElement(bindParam)); + if(clobBlob != null) + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "cannot combine in/not in and clob/blob", bindParam); + bindParams.add(new SpecialVariableElement(bindParam, SpecialVariableElement.SpecialType.IN_LIST)); final boolean not = bindParamMatcher.group(4) != null; final String replacement; switch (databaseType) { @@ -632,34 +650,37 @@ public class JdbcMapperProcessor extends AbstractProcessor { 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(\""); + if (param instanceof SpecialVariableElement) { + final SpecialVariableElement specialParam = (SpecialVariableElement) param; + switch (specialParam.specialType) { + case IN_LIST: { + 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(), specialParam.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(), specialParam.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()... + // 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; @@ -677,51 +698,50 @@ public class JdbcMapperProcessor extends AbstractProcessor { 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)) { - w.write("try {\n\t\t\t\tps.setBlob("); - w.write(Integer.toString(index)); - w.write(", "); - w.write(variableName); - w.write(" == null ? null : new java.io.ByteArrayInputStream("); - w.write(variableName); - w.write(".getBytes(\""); - w.write(blob.charsetName()); - w.write("\")));\n\t\t\t} catch (java.io.UnsupportedEncodingException e) {\n" + - "\t\t\t\tthrow new SQLException(\"String to Blob UnsupportedEncodingException\", e);\n" + - "\t\t\t}\n"); - return; - } else if (!(types.isAssignable(o, inputStreamType) || types.isAssignable(o, blobType) || types.isAssignable(o, fileType) || types.isAssignable(o, byteArrayType))) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.Blob only valid for String, byte[], Blob, InputStream, and File", param); - return; - } - } else { - final JdbcMapper.Clob clob = param.getAnnotation(JdbcMapper.Clob.class); - if (clob != null) { - if (types.isAssignable(o, stringType)) { - method = "Clob"; - variableName = variableName + " == null ? null : new java.io.StringReader(" + variableName + ")"; - } else if (!(types.isAssignable(o, readerType) || types.isAssignable(o, clobType))) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.Clob only valid for String, Clob, Reader", param); + //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!!", specialParam.delegate); + } + w.write(type); + w.write("\", "); + w.write(variableName); + if (collection) + w.write(".toArray()"); + w.write("));\n"); return; } + case BLOB: { + if (types.isAssignable(o, stringType)) { + w.write("try {\n\t\t\t\tps.setBlob("); + w.write(Integer.toString(index)); + w.write(", "); + w.write(variableName); + w.write(" == null ? null : new java.io.ByteArrayInputStream("); + w.write(variableName); + w.write(".getBytes(\""); + w.write(specialParam.blobStringCharset == null ? "UTF-8" : specialParam.blobStringCharset); + w.write("\")));\n\t\t\t} catch (java.io.UnsupportedEncodingException e) {\n" + + "\t\t\t\tthrow new SQLException(\"String to Blob UnsupportedEncodingException\", e);\n" + + "\t\t\t}\n"); + return; + } else if (!(types.isAssignable(o, inputStreamType) || types.isAssignable(o, blobType) || types.isAssignable(o, fileType) || types.isAssignable(o, byteArrayType))) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.Blob only valid for String, byte[], Blob, InputStream, and File", specialParam.delegate); + return; + } + } + case CLOB: { + if (types.isAssignable(o, stringType)) { + method = "Clob"; + variableName = variableName + " == null ? null : new java.io.StringReader(" + variableName + ")"; + } else if (!(types.isAssignable(o, readerType) || types.isAssignable(o, clobType))) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.Clob only valid for String, Clob, Reader", specialParam.delegate); + return; + } + } } } // end special behavior diff --git a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SimpleSQLChecker.java b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SimpleSQLChecker.java index 4a9799a..cf9d708 100644 --- a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SimpleSQLChecker.java +++ b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SimpleSQLChecker.java @@ -96,33 +96,33 @@ public class SimpleSQLChecker implements SQLChecker { public Object getFakeBindParam(final VariableElement param, final Connection conn, final ArrayInList arrayInList) throws SQLException { final TypeMirror o = param.asType(); // special behavior - if (param instanceof InListVariableElement) { - final TypeMirror componentType; - if (o.getKind() == TypeKind.ARRAY) { - componentType = ((ArrayType) o).getComponentType(); - } else if (o.getKind() == TypeKind.DECLARED && types.isAssignable(o, collectionType)) { - final DeclaredType dt = (DeclaredType) o; - componentType = dt.getTypeArguments().get(0); - } else { - getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, "invalid in-list-variable type, returning null", ((InListVariableElement) param).delegate); - return null; - } - if (types.isAssignable(componentType, numberType)) { - return arrayInList.toArray(conn, true, new Long[]{0L}); // todo: not quite right, oh well for now - } else if (types.isAssignable(componentType, stringType) || types.isAssignable(componentType, enumType)) { - return arrayInList.toArray(conn, false, new String[]{defaultString}); - } else { - getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, "invalid in-list-variable type, returning null", ((InListVariableElement) param).delegate); - return null; - } - } - final JdbcMapper.Blob blob = param.getAnnotation(JdbcMapper.Blob.class); - if (blob != null) { - return new ByteArrayInputStream(new byte[1]); - } else { - final JdbcMapper.Clob clob = param.getAnnotation(JdbcMapper.Clob.class); - if (clob != null) { - return new StringReader(defaultString); + if (param instanceof SpecialVariableElement) { + final SpecialVariableElement specialParam = (SpecialVariableElement) param; + switch (specialParam.specialType) { + case IN_LIST: { + final TypeMirror componentType; + if (o.getKind() == TypeKind.ARRAY) { + componentType = ((ArrayType) o).getComponentType(); + } else if (o.getKind() == TypeKind.DECLARED && types.isAssignable(o, collectionType)) { + final DeclaredType dt = (DeclaredType) o; + componentType = dt.getTypeArguments().get(0); + } else { + getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, "invalid in-list-variable type, returning null", ((SpecialVariableElement) param).delegate); + return null; + } + if (types.isAssignable(componentType, numberType)) { + return arrayInList.toArray(conn, true, new Long[]{0L}); // todo: not quite right, oh well for now + } else if (types.isAssignable(componentType, stringType) || types.isAssignable(componentType, enumType)) { + return arrayInList.toArray(conn, false, new String[]{defaultString}); + } else { + getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, "invalid in-list-variable type, returning null", ((SpecialVariableElement) param).delegate); + return null; + } + } + case BLOB: + return new ByteArrayInputStream(new byte[1]); + case CLOB: + return new StringReader(defaultString); } } // end special behavior diff --git a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/InListVariableElement.java b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SpecialVariableElement.java similarity index 72% rename from jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/InListVariableElement.java rename to jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SpecialVariableElement.java index cc72e99..bc249bc 100644 --- a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/InListVariableElement.java +++ b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SpecialVariableElement.java @@ -9,12 +9,26 @@ import java.util.Set; /** * Created by mopar on 6/1/17. */ -class InListVariableElement implements VariableElement { +class SpecialVariableElement implements VariableElement { + + enum SpecialType { + IN_LIST, + CLOB, + BLOB, + } final VariableElement delegate; + final SpecialType specialType; + final String blobStringCharset; - InListVariableElement(final VariableElement delegate) { + SpecialVariableElement(final VariableElement delegate, final SpecialType specialType) { + this(delegate, specialType, null); + } + + SpecialVariableElement(final VariableElement delegate, final SpecialType specialType, final String blobStringCharset) { this.delegate = delegate; + this.specialType = specialType; + this.blobStringCharset = blobStringCharset; } @Override @@ -85,4 +99,11 @@ class InListVariableElement implements VariableElement { } //IFJAVA8_END + + @Override + public String toString() { + return "InListVariableElement{" + + "delegate=" + delegate + + '}'; + } } diff --git a/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/ParamPatternTest.java b/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/ParamPatternTest.java new file mode 100644 index 0000000..bf473da --- /dev/null +++ b/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/ParamPatternTest.java @@ -0,0 +1,39 @@ +package com.moparisthebest.jdbc.codegen; + +import org.junit.Test; + +import java.util.regex.Matcher; + +import static com.moparisthebest.jdbc.codegen.JdbcMapperProcessor.paramPattern; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class ParamPatternTest { + + @Test + public void testPattern() { + testMatch("{personNo}", null, null, null, null, null, null, "personNo"); + testMatch("{last_name IN lastNames}", "last_name IN ", "last_name", "IN ", null, null, null, "lastNames"); + testMatch("{last_name not in lastNames}", "last_name not in ", "last_name", "not in ", "not ", null, null, "lastNames"); + testMatch("{clob:comment}", null, null, null, null, "clob:", null, "comment"); + testMatch("{clob: comment}", null, null, null, null, "clob: ", null, "comment"); + testMatch("{blob: comment}", null, null, null, null, "blob: ", null, "comment"); + testMatch("{Blob: comment}", null, null, null, null, "Blob: ", null, "comment"); + testMatch("{blob:utf-16:comment}", null, null, null, null, "blob:utf-16:", "utf-16:", "comment"); + String blobCharset = "utf-16:"; + assertEquals("utf-16", blobCharset.substring(0, blobCharset.indexOf(':')).trim()); + blobCharset = "utf-16 : "; + assertEquals("utf-16", blobCharset.substring(0, blobCharset.indexOf(':')).trim()); + } + + private static void testMatch(final String sql, final String... expected) { + final Matcher bindParamMatcher = paramPattern.matcher(sql); + while (bindParamMatcher.find()) { + final String[] matches = new String[bindParamMatcher.groupCount()]; + for (int x = 0; x < matches.length; ) + matches[x] = bindParamMatcher.group(++x); + //System.out.println(bindParamMatcher.group() + ": " + java.util.Arrays.toString(matches)); + assertArrayEquals(bindParamMatcher.group(), expected, matches); + } + } +} 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 f216b30..c2d7363 100644 --- a/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAO.java +++ b/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAO.java @@ -42,8 +42,8 @@ public interface PersonDAO extends JdbcMapper { @JdbcMapper.SQL("UPDATE person SET first_name = {firstName} WHERE person_no = {personNo}") void setFirstNameBlob(byte[] firstName, long personNo) throws SQLException; - @JdbcMapper.SQL("UPDATE person SET first_name = {firstName} WHERE person_no = {personNo}") - void setFirstNameBlob(@JdbcMapper.Blob String firstName, long personNo) throws SQLException; + @JdbcMapper.SQL("UPDATE person SET first_name = {blob:firstName} WHERE person_no = {personNo}") + void setFirstNameBlob(String firstName, long personNo) throws SQLException; @JdbcMapper.SQL("SELECT person_no FROM person WHERE last_name = {lastName}") long getPersonNo(String lastName) throws SQLException; diff --git a/presto-sqlparser/src/test/java/com/moparisthebest/jdbc/codegen/PrestoPersonDAO.java b/presto-sqlparser/src/test/java/com/moparisthebest/jdbc/codegen/PrestoPersonDAO.java index a16f8f0..ff9c83e 100644 --- a/presto-sqlparser/src/test/java/com/moparisthebest/jdbc/codegen/PrestoPersonDAO.java +++ b/presto-sqlparser/src/test/java/com/moparisthebest/jdbc/codegen/PrestoPersonDAO.java @@ -42,8 +42,8 @@ public interface PrestoPersonDAO extends PersonDAO { @JdbcMapper.SQL("UPDATE person SET first_name = {firstName} WHERE person_no = {personNo}") void setFirstNameBlob(byte[] firstName, long personNo) throws SQLException; - @JdbcMapper.SQL("UPDATE person SET first_name = {firstName} WHERE person_no = {personNo}") - void setFirstNameBlob(@JdbcMapper.Blob String firstName, long personNo) throws SQLException; + @JdbcMapper.SQL("UPDATE person SET first_name = {blob:firstName} WHERE person_no = {personNo}") + void setFirstNameBlob(String firstName, long personNo) throws SQLException; @JdbcMapper.SQL("SELECT person_no FROM person WHERE last_name = {lastName}") long getPersonNo(String lastName) throws SQLException;