From d8f7e6df4a3ea568bd15cdfa07b531c676447064 Mon Sep 17 00:00:00 2001 From: moparisthebest Date: Sat, 27 May 2017 23:54:22 -0400 Subject: [PATCH] Initial JdbcMapper annotation support --- beehive-jdbc-mapper/pom.xml | 28 ++ .../classgen/AbstractSQLParser.java | 26 ++ .../moparisthebest/classgen/SQLParser.java | 27 ++ .../classgen/SimpleSQLParser.java | 39 ++ .../jdbc/AbstractRowMapper.java | 28 +- .../jdbc/CompilingRowToObjectMapper.java | 95 +++-- .../jdbc/RowToObjectMapper.java | 8 +- .../codegen/CompileTimeResultSetMapper.java | 184 ++++++++ .../jdbc/codegen/JdbcMapper.java | 76 ++++ .../jdbc/codegen/JdbcMapperFactory.java | 24 ++ .../jdbc/codegen/JdbcMapperProcessor.java | 396 ++++++++++++++++++ .../javax.annotation.processing.Processor | 1 + .../jdbc/codegen/JdbcMapperTest.java | 34 ++ .../jdbc/codegen/PersonDAO.java | 55 +++ .../jdbc/codegen/SqlParserTest.java | 49 +++ 15 files changed, 1013 insertions(+), 57 deletions(-) create mode 100644 beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/AbstractSQLParser.java create mode 100644 beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/SQLParser.java create mode 100644 beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/SimpleSQLParser.java create mode 100644 beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/codegen/CompileTimeResultSetMapper.java create mode 100644 beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapper.java create mode 100644 beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperFactory.java create mode 100644 beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java create mode 100644 beehive-jdbc-mapper/src/main/resources/META-INF.services/javax.annotation.processing.Processor create mode 100644 beehive-jdbc-mapper/src/test/java/com/moparisthebest/jdbc/codegen/JdbcMapperTest.java create mode 100644 beehive-jdbc-mapper/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAO.java create mode 100644 beehive-jdbc-mapper/src/test/java/com/moparisthebest/jdbc/codegen/SqlParserTest.java diff --git a/beehive-jdbc-mapper/pom.xml b/beehive-jdbc-mapper/pom.xml index 49aba29..27c8878 100644 --- a/beehive-jdbc-mapper/pom.xml +++ b/beehive-jdbc-mapper/pom.xml @@ -22,9 +22,37 @@ org.apache.derby derby test + true ${project.artifactId} + + + + maven-compiler-plugin + 3.1 + + 1.6 + 1.6 + true + + + + + my-testCompile + test-compile + + testCompile + + + + com.moparisthebest.jdbc.codegen.JdbcMapperProcessor + + + + + + diff --git a/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/AbstractSQLParser.java b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/AbstractSQLParser.java new file mode 100644 index 0000000..a22faf3 --- /dev/null +++ b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/AbstractSQLParser.java @@ -0,0 +1,26 @@ +package com.moparisthebest.classgen; + +/** + * Created by mopar on 5/25/17. + */ +public abstract class AbstractSQLParser implements SQLParser { + + private final String[] columnNames; + private final boolean isSelect; + + protected AbstractSQLParser(final String[] columnNames, final boolean isSelect) { + this.columnNames = columnNames; + this.isSelect = isSelect; + } + + @Override + public final String[] columnNames() { + return columnNames; + } + + @Override + public final boolean isSelect() { + return isSelect; + } + +} diff --git a/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/SQLParser.java b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/SQLParser.java new file mode 100644 index 0000000..9c2b20c --- /dev/null +++ b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/SQLParser.java @@ -0,0 +1,27 @@ +package com.moparisthebest.classgen; + +/** + * Created by mopar on 5/25/17. + */ +public interface SQLParser { + + /** + * Return SQLParser instance for given SQL + * + * @param sql SQL to parse + * @return instance with string parsed + */ + SQLParser parse(String sql); + + /** + * @return column names for select, with 1-based index like ResultSet, index 0 is always null, not used if isSelect() returns false + */ + String[] columnNames(); + + /** + * Return + * + * @return true for Select, if we will map to an object, false to call executeUpdate (insert/update/merge/?) + */ + boolean isSelect(); +} diff --git a/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/SimpleSQLParser.java b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/SimpleSQLParser.java new file mode 100644 index 0000000..6fc6ee4 --- /dev/null +++ b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/SimpleSQLParser.java @@ -0,0 +1,39 @@ +package com.moparisthebest.classgen; + +import java.util.regex.Pattern; + +/** + * Created by mopar on 5/25/17. + */ +public class SimpleSQLParser extends AbstractSQLParser { + + private static final Pattern aliasPattern = Pattern.compile("^.*\\."); + + public SimpleSQLParser() { + super(null, false); + } + + + private SimpleSQLParser(final String[] columnNames, final boolean isSelect) { + super(columnNames, isSelect); + } + + @Override + public SQLParser parse(String sql) { + sql = sql.toUpperCase(); + final boolean isSelect = sql.startsWith("SELECT"); + String[] columnNames = null; + if (isSelect) { + final String columns = sql.substring(sql.indexOf("SELECT") + 6, sql.indexOf("FROM")).trim(); + final String[] splitColumns = columns.split(","); + columnNames = new String[splitColumns.length + 1]; + int index = 0; + for (String column : splitColumns) { + final String[] singleSplit = column.split("\\s"); + // trim and remove leading table/alias + columnNames[++index] = aliasPattern.matcher(singleSplit[singleSplit.length - 1].trim()).replaceFirst(""); + } + } + return new SimpleSQLParser(columnNames, isSelect); + } +} diff --git a/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/AbstractRowMapper.java b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/AbstractRowMapper.java index 68bbe42..2e7a9c2 100644 --- a/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/AbstractRowMapper.java +++ b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/AbstractRowMapper.java @@ -58,34 +58,36 @@ public abstract class AbstractRowMapper implements RowMapper { * @param returnTypeClass Class to map ResultSet rows to. * @param cal Calendar instance for date/time values. */ - protected AbstractRowMapper(ResultSet resultSet, Class returnTypeClass, Calendar cal, Class mapKeyType) { + protected AbstractRowMapper(String[] keys, ResultSet resultSet, Class returnTypeClass, Calendar cal, Class mapKeyType) { _resultSet = resultSet; _returnTypeClass = returnTypeClass; _cal = cal; _mapKeyType = mapKeyType; - try { - _columnCount = resultSet.getMetaData().getColumnCount(); - } catch (SQLException e) { - throw new MapperException("RowToObjectMapper: SQLException: " + e.getMessage(), e); + if(keys == null) { + try { + _columnCount = resultSet.getMetaData().getColumnCount(); + } catch (SQLException e) { + throw new MapperException("RowToObjectMapper: SQLException: " + e.getMessage(), e); + } + } else { + this.keys = keys; + _columnCount = keys.length - 1; } mapOnlySecondColumn = _mapKeyType != null && _columnCount == 2; } + protected AbstractRowMapper(ResultSet resultSet, Class returnTypeClass, Calendar cal, Class mapKeyType) { + this(null, resultSet, returnTypeClass, cal, mapKeyType); + } + protected AbstractRowMapper(ResultSet resultSet, Class returnTypeClass, Calendar cal) { this(resultSet, returnTypeClass, cal, null); } protected AbstractRowMapper(String[] keys, Class returnTypeClass, Calendar cal, Class mapKeyType) { - _resultSet = null; - _returnTypeClass = returnTypeClass; - _cal = cal; - _mapKeyType = mapKeyType; - this.keys = keys; - _columnCount = keys.length - 1; - - mapOnlySecondColumn = _mapKeyType != null && _columnCount == 2; + this(keys, null, returnTypeClass, cal, mapKeyType); } /** diff --git a/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/CompilingRowToObjectMapper.java b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/CompilingRowToObjectMapper.java index 402c5e8..ef8e0d0 100644 --- a/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/CompilingRowToObjectMapper.java +++ b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/CompilingRowToObjectMapper.java @@ -2,11 +2,11 @@ package com.moparisthebest.jdbc; import com.moparisthebest.classgen.Compiler; +import java.io.IOException; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.sql.ResultSet; -import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.Calendar; import java.util.Map; @@ -57,9 +57,17 @@ public class CompilingRowToObjectMapper extends RowToObjectMapper { } } catch (SQLException e) { throw new MapperException("CachingRowToObjectMapper: SQLException: " + e.getMessage(), e); + } catch (IOException e) { + throw new MapperException("CachingRowToObjectMapper: IOException (should never happen?): " + e.getMessage(), e); } } + public CompilingRowToObjectMapper(final String[] keys, Class returnTypeClass, Calendar cal, Class mapValType, Class mapKeyType) { + super(keys,null, returnTypeClass, cal, mapValType, mapKeyType, false); + this.compiler = null; + this.resultSetToObject = null; + } + @Override public T mapRowToReturnType() throws SQLException { return resultSetToObject.toObject(_resultSet, _cal); @@ -138,7 +146,7 @@ public class CompilingRowToObjectMapper extends RowToObjectMapper { } // code generation down here - protected ResultSetToObject genClass() { + protected ResultSetToObject genClass() throws IOException { final String className = "CompilingMapper"; final String tType = typeFromName(_returnTypeClass); final String kType = typeFromName(_mapKeyType); @@ -153,11 +161,12 @@ public class CompilingRowToObjectMapper extends RowToObjectMapper { final StringBuilder java = new StringBuilder(header); //java.append("return null;\n"); gen(java, tType); + java.append("return ret;\n"); java.append(" }\n\n public ").append(kType).append(" getFirstColumn(final java.sql.ResultSet rs, final java.util.Calendar cal) throws java.sql.SQLException {\n "); if(_mapKeyType != null){ java.append("return "); - extractColumnValueString(java, 1, _tmf.getTypeId(_mapKeyType)); + extractColumnValueString(java, 1, _mapKeyType); } else { java.append("throw new com.moparisthebest.jdbc.MapperException(com.moparisthebest.jdbc.CompilingRowToObjectMapper.firstColumnError)"); } @@ -167,10 +176,10 @@ public class CompilingRowToObjectMapper extends RowToObjectMapper { return compiler.compile(className, java); } - protected void gen(final StringBuilder java, final String tType) { + public void gen(final Appendable java, final String tType) throws IOException { if(mapOnlySecondColumn){ - java.append("return "); + java.append("final ").append(tType).append(" ret = "); extractColumnValueString(java, 2, _tmf.getTypeId(_returnTypeClass)); java.append(";\n"); return; @@ -179,7 +188,7 @@ public class CompilingRowToObjectMapper extends RowToObjectMapper { lazyLoadConstructor(); if (resultSetConstructor) { - java.append("return new ").append(tType).append("(rs);\n"); + java.append("final ").append(tType).append(" ret = new ").append(tType).append("(rs);\n"); return; } @@ -197,8 +206,7 @@ public class CompilingRowToObjectMapper extends RowToObjectMapper { } } else // we want a generic object type for (int x = 1; x < columnLength; ++x) - java.append("ret.put(").append(escapeMapKeyString(keys[x].toLowerCase())).append(", rs.getObject(").append(x).append("));\n"); - java.append("return ret;\n"); + java.append("ret.put(").append(escapeMapKeyString(keys[x].toLowerCase())).append(", rs.getObject(").append(String.valueOf(x)).append("));\n"); return; } catch (Throwable e) { throw new MapperException(e.getClass().getName() + " when trying to create a Map extends RowToObjectMapper { } else if (componentType != null) // we want an array try { - java.append("final ").append(tType).append(" ret = new ").append(tType.substring(0, tType.length() - 1)).append(_columnCount).append("];\n"); + java.append("final ").append(tType).append(" ret = new ").append(tType.substring(0, tType.length() - 1)).append(String.valueOf(_columnCount)).append("];\n"); final int typeId = _tmf.getTypeId(componentType); for (int x = 0; x < _columnCount; ) { - java.append("ret[").append(x).append("] = "); + java.append("ret[").append(String.valueOf(x)).append("] = "); extractColumnValueString(java, ++x, typeId); java.append(";\n"); } - java.append("return ret;\n"); return; } catch (Throwable e) { throw new MapperException(e.getClass().getName() + " when trying to create a " @@ -230,7 +237,7 @@ public class CompilingRowToObjectMapper extends RowToObjectMapper { try { if (typeId != TypeMappingsFactory.TYPE_UNKNOWN) { - java.append("return "); + java.append("final ").append(tType).append(" ret = "); extractColumnValueString(java, 1, typeId); java.append(";\n"); return; @@ -243,7 +250,7 @@ public class CompilingRowToObjectMapper extends RowToObjectMapper { } */ // we could actually pull from first row like above and test it first and fail now, but maybe just failing during compilation is enough? - java.append("return (").append(tType).append(") "); + java.append("final ").append(tType).append(" ret = (").append(tType).append(") "); extractColumnValueString(java, 1, typeId); java.append(";\n"); return; @@ -299,7 +306,11 @@ public class CompilingRowToObjectMapper extends RowToObjectMapper { // if this resultObject is Finishable, call finish() if (Finishable.class.isAssignableFrom(_returnTypeClass)) java.append("ret.finish(rs);\n"); - java.append("return ret;\n"); + } + + + public void extractColumnValueString(final Appendable java, final int index, final Class resultType) throws IOException { + extractColumnValueString(java, index, _tmf.getTypeId(resultType)); } /** @@ -310,86 +321,86 @@ public class CompilingRowToObjectMapper extends RowToObjectMapper { * @return The extracted value * @throws java.sql.SQLException on error. */ - protected void extractColumnValueString(final StringBuilder java, final int index, final int resultType) { + public void extractColumnValueString(final Appendable java, final int index, final int resultType) throws IOException { switch (resultType) { case TypeMappingsFactory.TYPE_INT: - java.append("rs.getInt(").append(index).append(")"); + java.append("rs.getInt(").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_LONG: - java.append("rs.getLong(").append(index).append(")"); + java.append("rs.getLong(").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_FLOAT: - java.append("rs.getFloat(").append(index).append(")"); + java.append("rs.getFloat(").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_DOUBLE: - java.append("rs.getDouble(").append(index).append(")"); + java.append("rs.getDouble(").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_BYTE: - java.append("rs.getByte(").append(index).append(")"); + java.append("rs.getByte(").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_SHORT: - java.append("rs.getInt(").append(index).append(")"); + java.append("rs.getInt(").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_BOOLEAN: - java.append("getBooleanYN(rs, ").append(index).append(")"); + java.append("getBooleanYN(rs, ").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_INT_OBJ: - java.append("getObjectInt(rs, ").append(index).append(")"); + java.append("getObjectInt(rs, ").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_LONG_OBJ: - java.append("getObjectLong(rs, ").append(index).append(")"); + java.append("getObjectLong(rs, ").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_FLOAT_OBJ: - java.append("getObjectFloat(rs, ").append(index).append(")"); + java.append("getObjectFloat(rs, ").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_DOUBLE_OBJ: - java.append("getObjectDouble(rs, ").append(index).append(")"); + java.append("getObjectDouble(rs, ").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_BYTE_OBJ: - java.append("getObjectByte(rs, ").append(index).append(")"); + java.append("getObjectByte(rs, ").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_SHORT_OBJ: - java.append("getObjectShort(rs, ").append(index).append(")"); + java.append("getObjectShort(rs, ").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_BOOLEAN_OBJ: - java.append("getObjectBooleanYN(rs, ").append(index).append(")"); + java.append("getObjectBooleanYN(rs, ").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_STRING: case TypeMappingsFactory.TYPE_XMLBEAN_ENUM: - java.append("rs.getString(").append(index).append(")"); + java.append("rs.getString(").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_BIG_DECIMAL: - java.append("rs.getBigDecimal(").append(index).append(")"); + java.append("rs.getBigDecimal(").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_BYTES: - java.append("rs.getBytes(").append(index).append(")"); + java.append("rs.getBytes(").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_TIMESTAMP: - java.append("getTimestamp(rs, cal, ").append(index).append(")"); + java.append("getTimestamp(rs, cal, ").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_TIME: - java.append("getTime(rs, cal, ").append(index).append(")"); + java.append("getTime(rs, cal, ").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_SQLDATE: - java.append("getSqlDate(rs, cal, ").append(index).append(")"); + java.append("getSqlDate(rs, cal, ").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_DATE: - java.append("getUtilDate(rs, cal, ").append(index).append(")"); + java.append("getUtilDate(rs, cal, ").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_CALENDAR: - java.append("getCalendar(rs, cal, ").append(index).append(")"); + java.append("getCalendar(rs, cal, ").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_REF: - java.append("rs.getRef(").append(index).append(")"); + java.append("rs.getRef(").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_BLOB: - java.append("rs.getBlob(").append(index).append(")"); + java.append("rs.getBlob(").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_CLOB: - java.append("rs.getClob(").append(index).append(")"); + java.append("rs.getClob(").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_ARRAY: - java.append("rs.getArray(").append(index).append(")"); + java.append("rs.getArray(").append(String.valueOf(index)).append(")"); return; case TypeMappingsFactory.TYPE_READER: case TypeMappingsFactory.TYPE_STREAM: @@ -397,7 +408,7 @@ public class CompilingRowToObjectMapper extends RowToObjectMapper { case TypeMappingsFactory.TYPE_STRUCT: case TypeMappingsFactory.TYPE_UNKNOWN: // JAVA_TYPE (could be any), or REF - java.append("rs.getObject(").append(index).append(")"); + java.append("rs.getObject(").append(String.valueOf(index)).append(")"); return; default: throw new MapperException("internal error: unknown type ID: " + Integer.toString(resultType)); diff --git a/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/RowToObjectMapper.java b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/RowToObjectMapper.java index abf233c..667f952 100644 --- a/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/RowToObjectMapper.java +++ b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/RowToObjectMapper.java @@ -95,6 +95,10 @@ public class RowToObjectMapper extends AbstractRowMapper { this(resultSet, returnTypeClass, cal, mapValType, mapKeyType, false); } + public RowToObjectMapper(ResultSet resultSet, Class returnTypeClass, Calendar cal, Class mapValType, Class mapKeyType, boolean caseInsensitiveMap) { + this(null, resultSet, returnTypeClass, cal, mapValType, mapKeyType, caseInsensitiveMap); + } + /** * Create a new RowToObjectMapper. * @@ -102,8 +106,8 @@ public class RowToObjectMapper extends AbstractRowMapper { * @param returnTypeClass Class to map to. * @param cal Calendar instance for date/time mappings. */ - public RowToObjectMapper(ResultSet resultSet, Class returnTypeClass, Calendar cal, Class mapValType, Class mapKeyType, boolean caseInsensitiveMap) { - super(resultSet, returnTypeClass, cal, mapKeyType); + public RowToObjectMapper(String[] keys, ResultSet resultSet, Class returnTypeClass, Calendar cal, Class mapValType, Class mapKeyType, boolean caseInsensitiveMap) { + super(keys, resultSet, returnTypeClass, cal, mapKeyType); returnMap = Map.class.isAssignableFrom(returnTypeClass); if(returnMap){ Class rtc = ResultSetMapper.getConcreteClass(returnTypeClass, HashMap.class); diff --git a/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/codegen/CompileTimeResultSetMapper.java b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/codegen/CompileTimeResultSetMapper.java new file mode 100644 index 0000000..845f58d --- /dev/null +++ b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/codegen/CompileTimeResultSetMapper.java @@ -0,0 +1,184 @@ +package com.moparisthebest.jdbc.codegen; + +import com.moparisthebest.jdbc.CompilingRowToObjectMapper; +import com.moparisthebest.jdbc.ResultSetMapper; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.*; + +import static com.moparisthebest.jdbc.codegen.JdbcMapperProcessor.typeMirrorToClass; + +/** + * Created by mopar on 5/26/17. + */ +public class CompileTimeResultSetMapper { + + @SuppressWarnings({"unchecked"}) + public void mapToResultType(final Writer w, final String[] keys, final ExecutableElement eeMethod, final int arrayMaxLength, final Calendar cal) throws IOException, NoSuchMethodException, ClassNotFoundException { + final Method m = fromExecutableElement(eeMethod); + final Class returnType = m.getReturnType(); + final TypeMirror returnTypeMirror = eeMethod.getReturnType(); + if (returnType.isArray()) { + toArray(w, keys, ((ArrayType) returnTypeMirror).getComponentType(), returnType.getComponentType(), arrayMaxLength, cal); + } else if (Collection.class.isAssignableFrom(returnType)) { + toCollection(w, keys, returnTypeMirror, returnType, ((DeclaredType) returnTypeMirror).getTypeArguments().get(0), (Class) getActualTypeArguments(m)[0], arrayMaxLength, cal); + } else if (Map.class.isAssignableFrom(returnType)) { + final List typeArguments = ((DeclaredType) returnTypeMirror).getTypeArguments(); + final Type[] types = getActualTypeArguments(m); + if (types[1] instanceof ParameterizedType) { // for collectionMaps + final ParameterizedType pt = (ParameterizedType) types[1]; + final Class collectionType = (Class) pt.getRawType(); + if (Collection.class.isAssignableFrom(collectionType)) { + final TypeMirror collectionTypeMirror = typeArguments.get(1); + toMapCollection(w, keys, + returnTypeMirror, returnType, + typeArguments.get(0), (Class) types[0], + collectionTypeMirror, collectionType, + ((DeclaredType) collectionTypeMirror).getTypeArguments().get(0), (Class) pt.getActualTypeArguments()[0], + arrayMaxLength, cal); + return; + } + } + toMap(w, keys, returnTypeMirror, returnType, typeArguments.get(0), (Class) types[0], typeArguments.get(1), (Class) types[1], arrayMaxLength, cal); + } else if (Iterator.class.isAssignableFrom(returnType)) { + if (ListIterator.class.isAssignableFrom(returnType)) + toListIterator(w, keys, ((DeclaredType) returnTypeMirror).getTypeArguments().get(0), (Class) getActualTypeArguments(m)[0], arrayMaxLength, cal); + else + toIterator(w, keys, ((DeclaredType) returnTypeMirror).getTypeArguments().get(0), (Class) getActualTypeArguments(m)[0], arrayMaxLength, cal); + } else { + toObject(w, keys, returnTypeMirror, returnType, cal); + } + } + + public CompilingRowToObjectMapper getRowMapper(final String[] keys, Class returnTypeClass, Calendar cal, Class mapValType, Class mapKeyType) { + return new CompilingRowToObjectMapper(keys, returnTypeClass, cal, mapValType, mapKeyType); + } + + public void writeObject(final Writer w, final String[] keys, final TypeMirror returnTypeMirror, final Class returnType, final Calendar cal) throws IOException { + getRowMapper(keys, returnType, cal, null, null).gen(w, returnTypeMirror.toString()); + } + + public void toObject(final Writer w, final String[] keys, final TypeMirror returnTypeMirror, final Class returnType, final Calendar cal) throws IOException { + w.write("\t\t\tif(rs.next()) {\n"); + writeObject(w, keys, returnTypeMirror, returnType, cal); + w.write("\t\t\t\treturn ret;\n\t\t\t} else {\n\t\t\t\treturn null;\n\t\t\t}\n"); + } + + @SuppressWarnings("unchecked") + public , E> void writeCollection(final Writer w, final String[] keys, final String returnTypeString, Class collectionType, final TypeMirror componentTypeMirror, Class componentType, int arrayMaxLength, Calendar cal) throws IOException { + collectionType = (Class) ResultSetMapper.getConcreteClass(collectionType, ArrayList.class); + w.write("\t\t\tfinal "); + w.write(returnTypeString); + w.write(" _colret = new "); + w.write(collectionType.getCanonicalName()); + w.write(returnTypeString.substring(returnTypeString.indexOf('<'))); + w.write("();\n\t\t\twhile(rs.next()) {\n"); + writeObject(w, keys, componentTypeMirror, componentType, cal); + w.write("\t\t\t\t_colret.add(ret);\n\t\t\t}\n"); + } + + public , E> void toCollection(final Writer w, final String[] keys, final TypeMirror collectionTypeMirror, Class collectionType, final TypeMirror componentTypeMirror, Class componentType, int arrayMaxLength, Calendar cal) throws IOException { + writeCollection(w, keys, collectionTypeMirror.toString(), collectionType, componentTypeMirror, componentType, arrayMaxLength, cal); + w.write("\t\t\treturn _colret;\n"); + } + + public void toArray(final Writer w, final String[] keys, final TypeMirror componentTypeMirror, Class componentType, int arrayMaxLength, Calendar cal) throws IOException { + final String returnTypeString = componentTypeMirror.toString(); + writeCollection(w, keys, "java.util.List<" + returnTypeString + ">", ArrayList.class, componentTypeMirror, componentType, arrayMaxLength, cal); + w.write("\t\t\treturn _colret.toArray(new "); + w.write(returnTypeString); + w.write("[_colret.size()]);\n"); + } + + public void toListIterator(final Writer w, final String[] keys, final TypeMirror componentTypeMirror, Class componentType, int arrayMaxLength, Calendar cal) throws IOException { + final String returnTypeString = componentTypeMirror.toString(); + writeCollection(w, keys, "java.util.List<" + returnTypeString + ">", ArrayList.class, componentTypeMirror, componentType, arrayMaxLength, cal); + w.write("\t\t\treturn _colret.listIterator();\n"); + } + + public void toIterator(final Writer w, final String[] keys, final TypeMirror componentTypeMirror, Class componentType, int arrayMaxLength, Calendar cal) throws IOException { + final String returnTypeString = componentTypeMirror.toString(); + writeCollection(w, keys, "java.util.List<" + returnTypeString + ">", ArrayList.class, componentTypeMirror, componentType, arrayMaxLength, cal); + w.write("\t\t\treturn _colret.iterator();\n"); + } + + @SuppressWarnings("unchecked") + public , K, E> void toMap(final Writer w, final String[] keys, final TypeMirror mapTypeMirror, Class mapType, final TypeMirror mapKeyTypeMirror, Class mapKeyType, final TypeMirror componentTypeMirror, Class componentType, int arrayMaxLength, Calendar cal) throws IOException { + mapType = (Class) ResultSetMapper.getConcreteClass(mapType, HashMap.class); + final String returnTypeString = mapTypeMirror.toString(); + w.write("\t\t\tfinal "); + w.write(returnTypeString); + w.write(" _colret = new "); + w.write(mapType.getCanonicalName()); + w.write(returnTypeString.substring(returnTypeString.indexOf('<'))); + w.write("();\n\t\t\twhile(rs.next()) {\n"); + + //writeObject(w, keys, componentTypeMirror, componentType, cal); + final CompilingRowToObjectMapper rm = getRowMapper(keys, componentType, cal, null, mapKeyType); + rm.gen(w, componentTypeMirror.toString()); + w.write("\t\t\t\t_colret.put("); + rm.extractColumnValueString(w, 1, mapKeyType); + w.write(", ret);\n\t\t\t}\n"); + w.write("\t\t\treturn _colret;\n"); + } + + @SuppressWarnings("unchecked") + public , K, E extends Collection, C> void toMapCollection(final Writer w, final String[] keys, + final TypeMirror mapTypeMirror, Class mapType, + final TypeMirror mapKeyTypeMirror, Class mapKeyType, + final TypeMirror collectionTypeMirror, Class collectionType, + final TypeMirror componentTypeMirror, Class componentType, + int arrayMaxLength, Calendar cal) throws IOException { + mapType = (Class) ResultSetMapper.getConcreteClass(mapType, HashMap.class); + collectionType = (Class) ResultSetMapper.getConcreteClass(collectionType, ArrayList.class); + final String returnTypeString = mapTypeMirror.toString(); + final String collectionTypeString = collectionTypeMirror.toString(); + w.write("\t\t\tfinal "); + w.write(returnTypeString); + w.write(" _colret = new "); + w.write(mapType.getCanonicalName()); + w.write(returnTypeString.substring(returnTypeString.indexOf('<'))); + w.write("();\n\t\t\twhile(rs.next()) {\n"); + + //writeObject(w, keys, componentTypeMirror, componentType, cal); + final CompilingRowToObjectMapper rm = getRowMapper(keys, componentType, cal, null, mapKeyType); + rm.gen(w, componentTypeMirror.toString()); + + w.write("\t\t\t\tfinal "); + w.write(mapKeyTypeMirror.toString()); + w.write(" _colkey = "); + rm.extractColumnValueString(w, 1, mapKeyType); + w.write(";\n\t\t\t\t"); + + w.write(collectionTypeString); + w.write(" _collist = _colret.get(_colkey);\n\t\t\t\tif(_collist == null) {\n\t\t\t\t\t_collist = new "); + w.write(collectionType.getCanonicalName()); + w.write(collectionTypeString.substring(collectionTypeString.indexOf('<'))); + w.write("();\n\t\t\t\t\t_colret.put(_colkey, _collist);\n\t\t\t\t}\n\t\t\t\t_collist.add(ret);\n\t\t\t}\n\t\t\treturn _colret;\n"); + } + + private static Type[] getActualTypeArguments(Method m) { + return ((ParameterizedType) m.getGenericReturnType()).getActualTypeArguments(); + } + + @SuppressWarnings("unchecked") + private Method fromExecutableElement(final ExecutableElement eeMethod) throws ClassNotFoundException, NoSuchMethodException { + final Class c = Class.forName(eeMethod.getEnclosingElement().asType().toString()); + final List params = eeMethod.getParameters(); + final Class[] parameterTypes = new Class[params.size()]; + int count = -1; + for (final VariableElement param : params) { + parameterTypes[++count] = typeMirrorToClass(param.asType()); + } + return c.getDeclaredMethod(eeMethod.getSimpleName().toString(), parameterTypes); + } +} diff --git a/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapper.java b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapper.java new file mode 100644 index 0000000..1d07d66 --- /dev/null +++ b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapper.java @@ -0,0 +1,76 @@ +package com.moparisthebest.jdbc.codegen; + +import com.moparisthebest.classgen.AbstractSQLParser; +import com.moparisthebest.classgen.SQLParser; + +import java.io.Closeable; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.sql.Connection; + +/** + * Created by mopar on 5/24/17. + */ +public interface JdbcMapper extends Closeable { + + Connection getConnection(); + + @Override + void close(); + + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.TYPE}) + public @interface Mapper { + /** + * The jndi name of the DataSource. This is a required element for this annotation. + */ + String jndiName() default ""; + + boolean cachePreparedStatements() default true; + + Class sqlParser() default AbstractSQLParser.class; + } + + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.METHOD}) + public @interface SQL { + /** + * The SQL statement to send to the database. Required annotation element. + */ + String value(); + + OptionalBool cachePreparedStatement() default OptionalBool.INHERIT; + + /** + * Maximum array length. + * Optional element. + * This element has no effect on the call unless the method return type is an array. + * When used in conjunction with the maxRows element, the size of the array generated + * from the result set will be the smaller of maxRows and arrayMaxLength. + *

+ * arrayMaxLength's default value is 1024, but may be set to zero to specify that + * there is no size limit for the array generated from the ResultSet. + * Since the generated array is stored in-memory, care should be taken when dealing + * with very large ResultSets when the value of this element is set to zero. + */ + int arrayMaxLength() default -1; + } + + public enum OptionalBool { + INHERIT, + TRUE, + FALSE; + + public boolean combine(final boolean inherited) { + switch (this) { + case TRUE: + return true; + case FALSE: + return false; + } + return inherited; + } + } +} diff --git a/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperFactory.java b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperFactory.java new file mode 100644 index 0000000..fe6c2ae --- /dev/null +++ b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperFactory.java @@ -0,0 +1,24 @@ +package com.moparisthebest.jdbc.codegen; + +import java.sql.Connection; + +/** + * Created by mopar on 5/24/17. + */ +public abstract class JdbcMapperFactory { + + static final String SUFFIX = "Bean"; + + @SuppressWarnings("unchecked") + public static T create(final Class jdbcMapper, final Connection connection) { + try { + return (T) Class.forName(jdbcMapper.getName() + SUFFIX).getConstructor(Connection.class).newInstance(connection); + } catch (Throwable e) { + throw new RuntimeException("could not create JdbcMapper, did the processor run at compile time?", e); + } + } + + public static T create(final Class jdbcMapper) { + return create(jdbcMapper, null); + } +} diff --git a/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java new file mode 100644 index 0000000..91fa36c --- /dev/null +++ b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java @@ -0,0 +1,396 @@ +package com.moparisthebest.jdbc.codegen; + +import com.moparisthebest.classgen.SQLParser; +import com.moparisthebest.classgen.SimpleSQLParser; + +import javax.annotation.processing.*; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.*; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import javax.tools.Diagnostic; +import java.io.*; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.SQLException; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.moparisthebest.jdbc.TryClose.tryClose; + +/** + * Created by mopar on 5/24/17. + */ +@SupportedAnnotationTypes("com.moparisthebest.jdbc.codegen.JdbcMapper.Mapper") +@SupportedSourceVersion(SourceVersion.RELEASE_5) +public class JdbcMapperProcessor extends AbstractProcessor { + + private static final Pattern paramPattern = Pattern.compile("\\{([^}]+)\\}"); + private static final CompileTimeResultSetMapper rsm = new CompileTimeResultSetMapper(); + + private Types types; + private TypeMirror sqlExceptionType, stringType, numberType, utilDateType, readerType, clobType, + byteArrayType, inputStreamType, fileType, blobType + //, clobStringType, blobStringType, arrayListObjectType + ; + + public JdbcMapperProcessor() { + //out.println("JdbcMapperProcessor running!"); + } + + @Override + public synchronized void init(final ProcessingEnvironment processingEnv) { + super.init(processingEnv); + this.types = processingEnv.getTypeUtils(); + final Elements elements = processingEnv.getElementUtils(); + sqlExceptionType = elements.getTypeElement(SQLException.class.getCanonicalName()).asType(); + stringType = elements.getTypeElement(String.class.getCanonicalName()).asType(); + numberType = elements.getTypeElement(Number.class.getCanonicalName()).asType(); + utilDateType = elements.getTypeElement(java.util.Date.class.getCanonicalName()).asType(); + readerType = elements.getTypeElement(Reader.class.getCanonicalName()).asType(); + clobType = elements.getTypeElement(Clob.class.getCanonicalName()).asType(); + inputStreamType = elements.getTypeElement(InputStream.class.getCanonicalName()).asType(); + fileType = elements.getTypeElement(File.class.getCanonicalName()).asType(); + blobType = elements.getTypeElement(Blob.class.getCanonicalName()).asType(); + // throws NPE: + //byteArrayType = elements.getTypeElement(byte[].class.getCanonicalName()).asType(); + //byteArrayType = this.types.getArrayType(elements.getTypeElement(byte.class.getCanonicalName()).asType()); + //byteArrayType = elements.getTypeElement(byte.class.getCanonicalName()).asType(); + byteArrayType = types.getArrayType(types.getPrimitiveType(TypeKind.BYTE)); + /* + clobStringType = elements.getTypeElement(ClobString.class.getCanonicalName()).asType(); + blobStringType = elements.getTypeElement(BlobString.class.getCanonicalName()).asType(); + arrayListObjectType = elements.getTypeElement(ArrayInList.ArrayListObject.class.getCanonicalName()).asType(); + */ + } + + @Override + public boolean process(final Set annotations, final RoundEnvironment roundEnv) { + //if(true) return false; + if (annotations.isEmpty() || roundEnv.processingOver()) { + // write out test classes + return false; + } + if (isInitialized()) + //System.out.println("annotations: " + annotations); + //System.out.println("roundEnv: " + roundEnv); + for (final Element element : roundEnv.getElementsAnnotatedWith(JdbcMapper.Mapper.class)) + try { + if (element.getKind() != ElementKind.CLASS && element.getKind() != ElementKind.INTERFACE && !element.getModifiers().contains(Modifier.ABSTRACT)) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.Mapper can only annotate an interface or abstract class", element); + continue; + } + final TypeElement genClass = (TypeElement) element; + final JdbcMapper.Mapper mapper = genClass.getAnnotation(JdbcMapper.Mapper.class); + final SQLParser parser = new SimpleSQLParser();//(SQLParser)Class.forName(mapper.sqlParser().getCanonicalName()).newInstance(); + final String qualifiedName = genClass.getQualifiedName().toString(); + final boolean isInterface = genClass.getKind().isInterface(); + final boolean doJndi = !mapper.jndiName().isEmpty(); + Writer w = null; + try { + w = processingEnv.getFiler().createSourceFile(qualifiedName + JdbcMapperFactory.SUFFIX).openWriter(); + final String packageName = ((PackageElement) genClass.getEnclosingElement()).getQualifiedName().toString(); + final String className = genClass.getSimpleName() + JdbcMapperFactory.SUFFIX; + if (!packageName.isEmpty()) { + w.write("package "); + w.write(packageName); + w.write(";\n\n"); + } + if (doJndi) { + w.write("import javax.naming.InitialContext;\n"); + w.write("import javax.sql.DataSource;\n"); + } + w.write("import java.sql.*;\n\n"); + w.write("import static com.moparisthebest.jdbc.TryClose.tryClose;\n\n"); + w.write("public class "); + w.write(className); + if (isInterface) { + w.write(" implements "); + } else { + w.write(" extends "); + } + w.write(genClass.getSimpleName().toString()); + w.write(" {\n\n\tprivate final Connection conn;\n"); + if (doJndi) { + w.write("\tprivate final InitialContext ctx;\n\n\tpublic "); + w.write(className); + w.write("() {\n\t\tthis(null);\n\t}\n"); + } + w.write("\n\tpublic "); + w.write(className); + w.write("(Connection conn) {\n\t\t"); + if (doJndi) { + w.write("InitialContext ctx = null;\n" + + "\t\tif (conn == null)\n" + + "\t\t\ttry {\n" + + "\t\t\t\tctx = new InitialContext();\n" + + "\t\t\t\tDataSource ds = (DataSource) ctx.lookup(\""); + w.write(mapper.jndiName()); // todo: escape this? I don't think anyone needs that, for now... + w.write("\");\n" + + "\t\t\t\tconn = ds.getConnection();\n" + + "\t\t\t} catch (Throwable e) {\n" + + "\t\t\t\ttryClose(ctx);\n" + + "\t\t\t\ttryClose(conn);\n" + + "\t\t\t\tthrow new RuntimeException(e);\n" + + "\t\t\t}\n" + + "\t\tthis.conn = conn;\n" + + "\t\tthis.ctx = ctx;" + ); + } else { + w.write("this.conn = conn;"); + } + w.write("\n\t\tif (this.conn == null)\n" + + "\t\t\tthrow new NullPointerException(\"Connection needs to be non-null for JdbcMapper...\");\n\t}\n" + + "\n\t@Override\n\tpublic Connection getConnection() {\n\t\treturn this.conn;\n\t}\n" + ); + + // loop through methods + final Types typeUtils = processingEnv.getTypeUtils(); + int cachedPreparedStatements = 0; + for (final Element methodElement : genClass.getEnclosedElements()) { + // can only implement abstract methods + if (methodElement.getKind() != ElementKind.METHOD || !methodElement.getModifiers().contains(Modifier.ABSTRACT)) + continue; + final ExecutableElement eeMethod = (ExecutableElement) methodElement; + final JdbcMapper.SQL sql = eeMethod.getAnnotation(JdbcMapper.SQL.class); + if (sql == null || sql.value().isEmpty()) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL with non-empty query is required on abstract or interface methods", methodElement); + continue; + } + w.write("\n\t@Override\n\tpublic "); + final String returnType = eeMethod.getReturnType().toString(); + w.write(returnType); + w.write(" "); + w.write(eeMethod.getSimpleName().toString()); + w.write('('); + + // build query and bind param order + final List bindParams = new ArrayList(); + final String sqlStatement; + boolean sqlExceptionThrown = false; + { + // now parameters + final List params = eeMethod.getParameters(); + final Map paramMap = new HashMap(params.size()); + final int numParams = params.size(); + int count = 0; + for (final VariableElement param : params) { + w.write("final "); + w.write(param.asType().toString()); + w.write(' '); + final String name = param.getSimpleName().toString(); + w.write(name); + paramMap.put(name, param); + if (++count != numParams) + w.write(", "); + } + + // throws? + w.write(")"); + final List thrownTypes = eeMethod.getThrownTypes(); + final int numThrownTypes = thrownTypes.size(); + if (numThrownTypes > 0) { + count = 0; + w.write(" throws "); + } + for (final TypeMirror thrownType : thrownTypes) { + sqlExceptionThrown |= typeUtils.isSameType(thrownType, sqlExceptionType); + w.write(thrownType.toString()); + if (++count != numThrownTypes) + w.write(", "); + } + w.write(" {\n"); + + final Matcher bindParamMatcher = paramPattern.matcher(sql.value()); + final StringBuffer sb = new StringBuffer(); + while (bindParamMatcher.find()) { + final String paramName = bindParamMatcher.group(1); + 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, "?"); + } + bindParamMatcher.appendTail(sb); + sqlStatement = sb.toString(); + } + + final SQLParser parsedSQl = parser.parse(sqlStatement); + // now implementation + w.write("\t\tPreparedStatement ps = null;\n"); + if (parsedSQl.isSelect()) + w.write("\t\tResultSet rs = null;\n"); + w.write("\t\ttry {\n\t\t\tps = "); + final boolean cachePreparedStatements = sql.cachePreparedStatement().combine(mapper.cachePreparedStatements()); + if (cachePreparedStatements) { + w.write("this.prepareStatement("); + w.write(Integer.toString(cachedPreparedStatements)); + w.write(", "); + ++cachedPreparedStatements; + } else { + w.write("conn.prepareStatement("); + } + w.write('"'); + w.write(sqlStatement); + w.write("\");\n"); + + // now bind parameters + int count = 0; + for (final VariableElement param : bindParams) + setObject(w, ++count, param.getSimpleName().toString(), param.asType()); + + if (!parsedSQl.isSelect()) { + if (returnType.equals("void")) { + w.write("\t\t\tps.executeUpdate();\n"); + } else if (returnType.equals("int") || returnType.equals("java.lang.Integer")) { + w.write("\t\t\treturn ps.executeUpdate();\n"); + } else { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL sql other than SELECT must return either void, int, or Integer", methodElement); + continue; + } + } else { + w.write("\t\t\trs = ps.executeQuery();\n"); + final String[] keys = parsedSQl.columnNames(); + if (keys == null || keys.length < 2) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL sql parsed no column names, proper SQL? Wildcard? or bad parser?", methodElement); + continue; + } + for (final String key : keys) + if ("*".equals(key)) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL sql parsed a wildcard column name which is not supported", methodElement); + return false; + } + rsm.mapToResultType(w, keys, eeMethod, sql.arrayMaxLength(), null); + } + + // if no SQLException is thrown, we have to catch it here and wrap it with RuntimeException... + if (!sqlExceptionThrown) { + w.write("\t\t} catch(SQLException e) {\n\t\t\tthrow new RuntimeException(e);\n"); + } + + // close things + w.write("\t\t} finally {\n"); + if (parsedSQl.isSelect()) + w.write("\t\t\ttryClose(rs);\n"); + if (!cachePreparedStatements) + w.write("\t\t\ttryClose(ps);\n"); + w.write("\t\t}\n"); + + w.write("\t}\n"); + } + + if (cachedPreparedStatements > 0) { + w.write("\n\tprivate final PreparedStatement[] psCache = new PreparedStatement["); + w.write(Integer.toString(cachedPreparedStatements)); + w.write("];\n\n\tprivate PreparedStatement prepareStatement(final int index, final String sql) throws SQLException {\n" + + "\t\tfinal PreparedStatement ps = psCache[index];\n" + + "\t\treturn ps == null ? (psCache[index] = conn.prepareStatement(sql)) : ps;\n" + + "\t}\n"); + } + + // close method + w.write("\n\t@Override\n\tpublic void close() {\n\t\t"); + if (cachedPreparedStatements > 0) + w.write("for(final PreparedStatement ps : psCache)\n\t\t\ttryClose(ps);\n\t\t"); + w.write("tryClose(conn);\n"); + if (doJndi) + w.write("\t\ttryClose(ctx);\n"); + w.write("\t}\n"); + // end close method + + w.write("}\n"); + } finally { + tryClose(w); + } + } catch (Exception e) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage(), element); + return false; + } + return true; + } + + private void setObject(final Writer w, final int index, String variableName, final TypeMirror o) throws SQLException, IOException { + w.write("\t\t\t"); + // we are going to put most common ones up top so it should execute faster normally + String method = null; + // todo: avoid string concat here + if (o.getKind().isPrimitive() || types.isAssignable(o, stringType) || types.isAssignable(o, numberType)) { + method = "Object"; + // java.util.Date support, put it in a Timestamp + } else if (types.isAssignable(o, utilDateType)) { + method = "Object"; + // might need to wrap with Timestamp + if (types.isSameType(o, utilDateType)) + variableName = "new java.sql.Timestamp(" + variableName + ".getTime())"; + // CLOB support + } else if (types.isAssignable(o, readerType) || types.isAssignable(o, clobType)) { + method = "Clob"; + /* + } else if (o instanceof ClobString) { + ps.setObject(index, ((ClobString) o).s == null ? null : ((ClobString) o).s); + */ + // BLOB support + } else if (types.isAssignable(o, byteArrayType)) { + method = "Blob"; + variableName = "new java.io.ByteArrayInputStream(" + variableName + ")"; + } else if (types.isAssignable(o, inputStreamType) || types.isAssignable(o, blobType)) { + method = "Blob"; + } else if (types.isAssignable(o, fileType)) { + // todo: does this close the InputStream properly???? + w.write("\t\t\ttry {\n" + + "\t\t\t\tps.setBlob(" + index + ", new java.io.FileInputStream(" + variableName + "));\n" + + "\t\t\t} catch (java.io.FileNotFoundException e) {\n" + + "\t\t\t\tthrow new SQLException(\"File to Blob FileNotFoundException\", e);\n" + + "\t\t\t}"); + return; + /* + } else if (o instanceof BlobString) { + try { + ps.setBlob(index, ((BlobString) o).s == null ? null : new ByteArrayInputStream(((BlobString) o).s.getBytes("UTF-8"))); + } catch (UnsupportedEncodingException e) { + throw new SQLException("String to Blob UnsupportedEncodingException", e); + } + } else if (o instanceof ArrayInList.ArrayListObject) { + ps.setArray(index, ((ArrayInList.ArrayListObject) o).getArray()); + */ + } else { + // probably won't get here ever, but just in case... + method = "Object"; + } + w.write("ps.set"); + w.write(method); + w.write('('); + w.write(Integer.toString(index)); + w.write(", "); + w.write(variableName); + w.write(");\n"); + } + + public static Class typeMirrorToClass(final TypeMirror tm) throws ClassNotFoundException { + switch (tm.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; + default: + return Class.forName(tm.toString()); + } + } +} diff --git a/beehive-jdbc-mapper/src/main/resources/META-INF.services/javax.annotation.processing.Processor b/beehive-jdbc-mapper/src/main/resources/META-INF.services/javax.annotation.processing.Processor new file mode 100644 index 0000000..5c38051 --- /dev/null +++ b/beehive-jdbc-mapper/src/main/resources/META-INF.services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +com.moparisthebest.jdbc.codegen.JdbcMapperProcessor diff --git a/beehive-jdbc-mapper/src/test/java/com/moparisthebest/jdbc/codegen/JdbcMapperTest.java b/beehive-jdbc-mapper/src/test/java/com/moparisthebest/jdbc/codegen/JdbcMapperTest.java new file mode 100644 index 0000000..3ca9483 --- /dev/null +++ b/beehive-jdbc-mapper/src/test/java/com/moparisthebest/jdbc/codegen/JdbcMapperTest.java @@ -0,0 +1,34 @@ +package com.moparisthebest.jdbc.codegen; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import static com.moparisthebest.jdbc.QueryMapperTest.fieldPerson1; +import static com.moparisthebest.jdbc.QueryMapperTest.getConnection; +import static com.moparisthebest.jdbc.TryClose.tryClose; +import static org.junit.Assert.assertEquals; + +/** + * Created by mopar on 5/24/17. + */ +public class JdbcMapperTest { + + private static PersonDAO dao; + + @BeforeClass + public static void setUp() throws Throwable { + dao = JdbcMapperFactory.create(PersonDAO.class, getConnection()); + //dao = new com.moparisthebest.jdbc.codegen.PersonDAOBean(getConnection()); + } + + @AfterClass + public static void tearDown() throws Throwable { + tryClose(dao); + } + + @Test + public void testName() throws Throwable { + assertEquals(fieldPerson1.getFirstName(), dao.getFirstName(fieldPerson1.getPersonNo())); + } +} diff --git a/beehive-jdbc-mapper/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAO.java b/beehive-jdbc-mapper/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAO.java new file mode 100644 index 0000000..0cffa41 --- /dev/null +++ b/beehive-jdbc-mapper/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAO.java @@ -0,0 +1,55 @@ +package com.moparisthebest.jdbc.codegen; + +import com.moparisthebest.jdbc.dto.FieldPerson; + +import java.sql.SQLException; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +/** + * Created by mopar on 5/24/17. + */ +@JdbcMapper.Mapper( +// jndiName = "bob", + cachePreparedStatements = false +) +public interface PersonDAO extends JdbcMapper { + + @JdbcMapper.SQL("UPDATE person SET first_name = {firstName} WHERE last_name = {lastName}") + int setFirstName(String firstName, String lastName); + + @JdbcMapper.SQL("UPDATE person SET first_name = {firstName} WHERE person_no = {personNo}") + void setFirstName(String firstName, long personNo) throws SQLException; + + @JdbcMapper.SQL("UPDATE person SET first_name = {firstName} WHERE person_no = {personNo}") + void setFirstNameBlob(byte[] firstName, long personNo) throws SQLException; + + @JdbcMapper.SQL("SELECT first_name FROM person WHERE person_no = {personNo}") + String getFirstName(long personNo) throws SQLException; + + @JdbcMapper.SQL("SELECT first_name, last_name FROM person WHERE person_no = {personNo}") + FieldPerson getPerson(long personNo) throws SQLException; + + @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 = {lastName}") + FieldPerson[] getPeopleArray(String lastName) throws SQLException; + + @JdbcMapper.SQL("SELECT first_name, last_name FROM person WHERE last_name = {lastName}") + Iterator getPeopleIterator(String lastName) throws SQLException; + + @JdbcMapper.SQL("SELECT first_name, last_name FROM person WHERE last_name = {lastName}") + ListIterator getPeopleListIterator(String lastName) throws SQLException; + + @JdbcMapper.SQL("SELECT first_name, last_name, person_no FROM person WHERE last_name = {lastName}") + Map getPersonMap(String lastName) throws SQLException; + + @JdbcMapper.SQL("SELECT first_name, last_name, person_no FROM person WHERE last_name = {lastName}") + Map> getPersonMapList(String lastName) throws SQLException; + + @JdbcMapper.SQL("SELECT first_name FROM person WHERE person_no = {personNo} and last_name = {lastName}") + String getFirstName(long personNo, String lastName) throws SQLException; +} diff --git a/beehive-jdbc-mapper/src/test/java/com/moparisthebest/jdbc/codegen/SqlParserTest.java b/beehive-jdbc-mapper/src/test/java/com/moparisthebest/jdbc/codegen/SqlParserTest.java new file mode 100644 index 0000000..dc5d12b --- /dev/null +++ b/beehive-jdbc-mapper/src/test/java/com/moparisthebest/jdbc/codegen/SqlParserTest.java @@ -0,0 +1,49 @@ +package com.moparisthebest.jdbc.codegen; + +import com.moparisthebest.classgen.SQLParser; +import com.moparisthebest.classgen.SimpleSQLParser; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Created by mopar on 5/30/17. + */ +public class SqlParserTest { + + private final SQLParser factory = new SimpleSQLParser(); + + @Test + public void testSingleSelect() { + final SQLParser ret = factory.parse("select bob from tom"); + assertTrue(ret.isSelect()); + assertArrayEquals(new String[]{null, "BOB"}, ret.columnNames()); + } + + @Test + public void testMultiSelect() { + final String[] expected = new String[]{null, "BOB", "TOM"}; + for (final String sql : new String[]{ + "select bob, tom from tom" + , "select some_bob bob, some_tom as tom from tom" + , "select tom.bob, some_tom as tom from tom" + }) { + final SQLParser ret = factory.parse(sql); + assertTrue(ret.isSelect()); + assertArrayEquals(expected, ret.columnNames()); + } + } + + @Test + public void testNotSelect() { + for (final String sql : new String[]{ + "UPDATE bob SET bob = 'bob' WHERE bob_no = 1" + , "INSERT INTO bob (bob_no, bob) VALUES (1, 'bob')" + , "MERGE INTO bob bla bla bla" + }) { + final SQLParser ret = factory.parse(sql); + assertFalse(ret.isSelect()); + } + } + +}