Remove Blob and Clob annotations in favor of SQL bind syntax

This commit is contained in:
Travis Burtrum 2018-03-14 02:25:11 -04:00
parent ba7fb733b2
commit edd6fc6e65
7 changed files with 187 additions and 136 deletions

View File

@ -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
*/

View File

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

View File

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

View File

@ -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 +
'}';
}
}

View File

@ -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);
}
}
}

View File

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

View File

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