diff --git a/querymapper/src/main/java/com/moparisthebest/jdbc/ArrayInList.java b/common/src/main/java/com/moparisthebest/jdbc/ArrayInList.java similarity index 87% rename from querymapper/src/main/java/com/moparisthebest/jdbc/ArrayInList.java rename to common/src/main/java/com/moparisthebest/jdbc/ArrayInList.java index 84306b7..0feb51b 100644 --- a/querymapper/src/main/java/com/moparisthebest/jdbc/ArrayInList.java +++ b/common/src/main/java/com/moparisthebest/jdbc/ArrayInList.java @@ -55,29 +55,16 @@ public class ArrayInList implements InList { } public InListObject inList(final Connection conn, final String columnName, final Collection values) throws SQLException { - return values == null || values.isEmpty() ? InListObject.inEmpty : new ArrayListObject( + return values == null || values.isEmpty() ? InListObject.inEmpty : new InListObject( columnAppendIn(columnName), toArray(conn, values) ); } public InListObject notInList(final Connection conn, final String columnName, final Collection values) throws SQLException { - return values == null || values.isEmpty() ? InListObject.notInEmpty : new ArrayListObject( + return values == null || values.isEmpty() ? InListObject.notInEmpty : new InListObject( columnAppendNotIn(columnName), toArray(conn, values) ); } - - class ArrayListObject extends InListObject { - private final Array array; - - public ArrayListObject(final String sql, final Array array) { - super(sql); - this.array = array; - } - - public Array getArray() { - return array; - } - } } diff --git a/querymapper/src/main/java/com/moparisthebest/jdbc/BindInList.java b/common/src/main/java/com/moparisthebest/jdbc/BindInList.java similarity index 75% rename from querymapper/src/main/java/com/moparisthebest/jdbc/BindInList.java rename to common/src/main/java/com/moparisthebest/jdbc/BindInList.java index 5cd4aaf..700d180 100644 --- a/querymapper/src/main/java/com/moparisthebest/jdbc/BindInList.java +++ b/common/src/main/java/com/moparisthebest/jdbc/BindInList.java @@ -34,29 +34,16 @@ public class BindInList implements InList { } public InListObject inList(final Connection conn, final String columnName, final Collection values) { - return values == null || values.isEmpty() ? InListObject.inEmpty : new BindInListObject( + return values == null || values.isEmpty() ? InListObject.inEmpty : new InListObject( toInList(columnName, values, this.maxSize), - values.toArray() + values ); } public InListObject notInList(final Connection conn, final String columnName, final Collection values) { - return values == null || values.isEmpty() ? InListObject.notInEmpty : new BindInListObject( + return values == null || values.isEmpty() ? InListObject.notInEmpty : new InListObject( toNotInList(columnName, values, this.maxSize), - values.toArray() + values ); } - - class BindInListObject extends InListObject { - private final Object[] bindObjects; - - public BindInListObject(final String sql, final Object[] bindObjects) { - super(sql); - this.bindObjects = bindObjects; - } - - public Object[] getBindObjects() { - return bindObjects; - } - } } diff --git a/common/src/main/java/com/moparisthebest/jdbc/InList.java b/common/src/main/java/com/moparisthebest/jdbc/InList.java new file mode 100644 index 0000000..4208618 --- /dev/null +++ b/common/src/main/java/com/moparisthebest/jdbc/InList.java @@ -0,0 +1,108 @@ +package com.moparisthebest.jdbc; + +import com.moparisthebest.jdbc.codegen.JdbcMapper; +import com.moparisthebest.jdbc.util.Bindable; +import com.moparisthebest.jdbc.util.InListUtil; +import com.moparisthebest.jdbc.util.PreparedStatementUtil; + +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collection; + +/** + * For a column name and a Collection, return a Object usable by QueryMapper for binding to a PreparedStatement and + * ListQueryMapper for substituting in the query + */ +public interface InList { + + InList defaultInList = InListObject.getDefaultInListInstance(); + + /** + * Returns an InList instance for use with this connection + * @param conn connection which may be inspected to determine best InList to use + * @return InList instance + */ + public InList instance(final Connection conn); + + /** + * Returns an Object who's .toString returns a String for a query, and QueryMapper knows how to bind to a PreparedStatement + * @param columnName Column name for query + * @param values values for in list + * @return object + */ + public InListObject inList(final Connection conn, final String columnName, final Collection values) throws SQLException; + + /** + * Returns an Object who's .toString returns a String for a query, and QueryMapper knows how to bind to a PreparedStatement + * @param columnName Column name for query + * @param values values for not in list + * @return object + */ + public InListObject notInList(final Connection conn, final String columnName, final Collection values) throws SQLException; + + class InListObject implements Bindable { + static final InListObject inEmpty = new InListObject(InListUtil.inEmpty, null); + static final InListObject notInEmpty = new InListObject(InListUtil.notInEmpty, null); + + public static InListObject inEmpty() { + return inEmpty; + } + + public static InListObject notInEmpty() { + return notInEmpty; + } + + private final String sql; + private final Object bindObject; + + public InListObject(final String sql, final Object bindObject) { + this.sql = sql; + this.bindObject = bindObject; + } + + @Override + public final String toString() { + return sql; + } + + @Override + public Object getBindObject() { + return bindObject; + } + + private static InList getDefaultInListInstance() { + try { + final String inListClassName = System.getProperty("QueryMapper.defaultInList.class"); + if(inListClassName != null) { + final Class inListClass = Class.forName(inListClassName); + final Method method = inListClass.getMethod(System.getProperty("QueryMapper.defaultInList.method", "instance")); + return (InList) method.invoke(null); + } else { + // todo: change default to OPTIMAL ? + final String type = System.getProperty("queryMapper.databaseType", System.getProperty("jdbcMapper.databaseType", "BIND")); + if(type.equals("OPTIMAL")) { + return OptimalInList.instance(); + } else { + switch (JdbcMapper.DatabaseType.valueOf(type)) { + case DEFAULT: + case BIND: + return BindInList.instance(); + case ANY: + return ArrayInList.instance(); + case ORACLE: + return OracleArrayInList.instance(); + case UNNEST: + return UnNestArrayInList.instance(); + default: + throw new RuntimeException("Invalid queryMapper.databaseType: " + type); + } + } + } + } catch (Throwable e) { + // NEVER ignore + throw new RuntimeException(e); + } + } + } +} diff --git a/querymapper/src/main/java/com/moparisthebest/jdbc/OptimalInList.java b/common/src/main/java/com/moparisthebest/jdbc/OptimalInList.java similarity index 100% rename from querymapper/src/main/java/com/moparisthebest/jdbc/OptimalInList.java rename to common/src/main/java/com/moparisthebest/jdbc/OptimalInList.java diff --git a/querymapper/src/main/java/com/moparisthebest/jdbc/OracleArrayInList.java b/common/src/main/java/com/moparisthebest/jdbc/OracleArrayInList.java similarity index 100% rename from querymapper/src/main/java/com/moparisthebest/jdbc/OracleArrayInList.java rename to common/src/main/java/com/moparisthebest/jdbc/OracleArrayInList.java diff --git a/querymapper/src/main/java/com/moparisthebest/jdbc/UnNestArrayInList.java b/common/src/main/java/com/moparisthebest/jdbc/UnNestArrayInList.java similarity index 100% rename from querymapper/src/main/java/com/moparisthebest/jdbc/UnNestArrayInList.java rename to common/src/main/java/com/moparisthebest/jdbc/UnNestArrayInList.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 9546bf5..cad0774 100644 --- a/common/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapper.java +++ b/common/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapper.java @@ -1,5 +1,7 @@ package com.moparisthebest.jdbc.codegen; +import com.moparisthebest.jdbc.util.SqlBuilder; + import java.io.Closeable; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -14,6 +16,14 @@ public interface JdbcMapper extends Closeable { Connection getConnection(); + //IFJAVA8_START + + default SqlBuilder sqlBuilder() { + return SqlBuilder.of(getConnection()); // todo: should this use the current inList ? + } + + //IFJAVA8_END + @Override void close(); diff --git a/common/src/main/java/com/moparisthebest/jdbc/util/Bindable.java b/common/src/main/java/com/moparisthebest/jdbc/util/Bindable.java new file mode 100644 index 0000000..5e6fc4e --- /dev/null +++ b/common/src/main/java/com/moparisthebest/jdbc/util/Bindable.java @@ -0,0 +1,31 @@ +package com.moparisthebest.jdbc.util; + +import static com.moparisthebest.jdbc.util.PreparedStatementUtil.noBind; + +public interface Bindable { + + Bindable empty = new Bindable() { + @Override + public Object getBindObject() { + return noBind; + } + + @Override + public String toString() { + return ""; + } + }; + + /** + * This returns raw SQL to be included in a query, can contain bind params as standard ? + * @return + */ + String toString(); + + /** + * This returns an object (or list of objects, or list of list etc) to bind to the SQL snippet returned by toString() + * PreparedStatementUtil must know how to bind this + * @return + */ + Object getBindObject(); +} diff --git a/common/src/main/java/com/moparisthebest/jdbc/util/PreparedStatementUtil.java b/common/src/main/java/com/moparisthebest/jdbc/util/PreparedStatementUtil.java new file mode 100644 index 0000000..5b30575 --- /dev/null +++ b/common/src/main/java/com/moparisthebest/jdbc/util/PreparedStatementUtil.java @@ -0,0 +1,201 @@ +package com.moparisthebest.jdbc.util; + +import com.moparisthebest.jdbc.InList; + +import java.io.*; +import java.nio.charset.Charset; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Collection; + +//IFJAVA8_START +import java.time.*; + +import static java.nio.charset.StandardCharsets.UTF_8; +//IFJAVA8_END + +public class PreparedStatementUtil { + + public static final Object noBind = new Object(); + + public static PreparedStatement bind(final PreparedStatement ps, final Object... bindObjects) throws SQLException { + recursiveBind(ps, bindObjects); + return ps; + } + + public static void setObject(final PreparedStatement ps, final int index, final Object o) throws SQLException { + // we are going to put most common ones up top so it should execute faster normally + if (o == null || o instanceof String || o instanceof Number) + ps.setObject(index, o); + // java.util.Date support, put it in a Timestamp + else if (o instanceof java.util.Date) + ps.setObject(index, o.getClass().equals(java.util.Date.class) ? new java.sql.Timestamp(((java.util.Date)o).getTime()) : o); + //IFJAVA8_START// todo: other java.time types + else if (o instanceof Instant) + ps.setObject(index, java.sql.Timestamp.from((Instant)o)); + else if (o instanceof LocalDateTime) + ps.setObject(index, java.sql.Timestamp.valueOf((LocalDateTime)o)); + else if (o instanceof LocalDate) + ps.setObject(index, java.sql.Date.valueOf((LocalDate)o)); + else if (o instanceof LocalTime) + ps.setObject(index, java.sql.Time.valueOf((LocalTime)o)); + else if (o instanceof ZonedDateTime) + ps.setObject(index, java.sql.Timestamp.from(((ZonedDateTime)o).toInstant())); + else if (o instanceof OffsetDateTime) + ps.setObject(index, java.sql.Timestamp.from(((OffsetDateTime)o).toInstant())); + else if (o instanceof OffsetTime) + ps.setObject(index, java.sql.Time.valueOf(((OffsetTime)o).toLocalTime())); // todo: no timezone? + + //IFJAVA8_END + // CLOB support + else if (o instanceof Reader) + ps.setClob(index, (Reader) o); + else if (o instanceof ClobString) + ps.setClob(index, ((ClobString) o).s == null ? null : new StringReader(((ClobString) o).s)); + else if (o instanceof java.sql.Clob) + ps.setClob(index, (java.sql.Clob) o); + // BLOB support + else if (o instanceof byte[]) + ps.setBlob(index, new ByteArrayInputStream((byte[]) o)); + else if (o instanceof InputStream) + ps.setBlob(index, (InputStream) o); + else if (o instanceof File) + try { + ps.setBlob(index, new FileInputStream((File) o)); // todo: does this close this or leak a file descriptor? + } catch (FileNotFoundException e) { + throw new SQLException("File to Blob FileNotFoundException", e); + } + else if (o instanceof BlobString) + ps.setBlob(index, ((BlobString) o).s == null ? null : new ByteArrayInputStream(((BlobString) o).s.getBytes(((BlobString) o).charset))); + else if (o instanceof java.sql.Blob) + ps.setBlob(index, (java.sql.Blob) o); + else if (o instanceof java.sql.Array) + ps.setArray(index, (java.sql.Array) o); + else if (o instanceof Enum) + ps.setObject(index, ((Enum)o).name()); + else + ps.setObject(index, o); // probably won't get here ever, but just in case... + /* + switch(ps.getParameterMetaData().getParameterType(index)){ // 'java.sql.SQLException: Unsupported feature', fully JDBC 3.0 compliant my ass, freaking oracle... + case Types.CLOB: + if(o instanceof String) + ps.setObject(index, o); + else if (o instanceof Reader) + ps.setClob(index, (Reader) o); + else if (o instanceof Clob) + ps.setClob(index, (Clob) o); + return; + case Types.BLOB: + if (o instanceof byte[]) + ps.setBlob(index, new ByteArrayInputStream((byte[])o)); + else if (o instanceof InputStream) + ps.setBlob(index, (InputStream) o); + else if (o instanceof File) + try { + ps.setBlob(index, new FileInputStream((File) o)); + } catch (FileNotFoundException e) { + throw new SQLException("File to Blob FileNotFoundException", e); + } + else if (o instanceof Blob) + ps.setBlob(index, (Blob) o); + else if(o instanceof String) + try{ + ps.setBlob(index, new ByteArrayInputStream(((String) o).getBytes("UTF-8"))); + }catch(UnsupportedEncodingException e){ + throw new SQLException("String to Blob UnsupportedEncodingException", e); + } + return; + default: + ps.setObject(index, o); + } + */ + } + + public static int recursiveBind(final PreparedStatement ps, final Object... bindObjects) throws SQLException { + return recursiveBindIndex(ps, 0, bindObjects); + } + + public static int recursiveBindIndex(final PreparedStatement ps, int index, final Object... bindObjects) throws SQLException { + if (bindObjects != null && bindObjects.length > 0) { + for (final Object o : bindObjects) { + if (o != null) { + if (o == InList.InListObject.inEmpty() || o == InList.InListObject.notInEmpty() || o == noBind) { + continue; // ignore + } else if (o instanceof Bindable) { + index = recursiveBindIndex(ps, index, ((Bindable) o).getBindObject()); + continue; + } else if (o instanceof Object[]) { + index = recursiveBindIndex(ps, index, (Object[]) o); + continue; + } else if (o instanceof Collection) { + // is creating 1 array and doing 1 method call faster than falling through to iterator and making multiple method calls/arrays? *probably* ? + index = recursiveBindIndex(ps, index, ((Collection) o).toArray()); + continue; + } else if(o instanceof Iterable) { + for(final Object o2 : (Iterable) o) { + index = recursiveBindIndex(ps, index, o2); + } + continue; + } + } + //System.out.printf("index: '%d' bound to '%s'\n", index+1, o); + setObject(ps, ++index, o); + //ps.setObject(++index, o); + } + } + return index; + } + + public static Object wrapClob(String s) { + return new ClobString(s); + } + + public static Object wrapBlob(String s) { + return new BlobString(s, UTF_8); + } + + public static Object wrapBlob(final String s, final Charset charset) { + return new BlobString(s, charset == null ? UTF_8 : charset); + } + + private static class StringWrapper { + public final String s; + + private StringWrapper(String s) { + this.s = s; + } + + public String toString() { + return s; + } + + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof StringWrapper)) return false; + StringWrapper that = (StringWrapper) o; + return !(s != null ? !s.equals(that.s) : that.s != null); + } + + public int hashCode() { + return s != null ? s.hashCode() : 0; + } + } + + private static class ClobString extends StringWrapper { + private ClobString(String s) { + super(s); + } + } + + private static class BlobString extends StringWrapper { + private final Charset charset; + private BlobString(final String s, final Charset charset) { + super(s); + this.charset = charset; + } + } + + /*IFJAVA6_START + private static final Charset UTF_8 = Charset.forName("UTF-8"); + IFJAVA6_END*/ +} diff --git a/common/src/main/java/com/moparisthebest/jdbc/util/SqlBuilder.java b/common/src/main/java/com/moparisthebest/jdbc/util/SqlBuilder.java new file mode 100644 index 0000000..6923ca5 --- /dev/null +++ b/common/src/main/java/com/moparisthebest/jdbc/util/SqlBuilder.java @@ -0,0 +1,463 @@ +package com.moparisthebest.jdbc.util; + +import com.moparisthebest.jdbc.*; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.*; + +//IFJAVA8_START +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.IntStream; +import java.util.stream.Stream; +//IFJAVA8_END + +import static com.moparisthebest.jdbc.InList.defaultInList; + +public class SqlBuilder implements Appendable, CharSequence, Collection, Bindable { + + private final StringBuilder sb; + private final Collection bindObjects; + + private final InList inList; + private final Connection conn; + + private SqlBuilder(final Connection conn, final InList inList, final StringBuilder sb, final Collection bindObjects) { + if(sb == null || bindObjects == null || inList == null || conn == null) + throw new NullPointerException("all arguments must be non-null"); + this.sb = sb; + this.bindObjects = bindObjects; + this.inList = inList.instance(conn); + this.conn = conn; + } + + public static SqlBuilder of(final Connection conn, final InList inList, final StringBuilder sb, final Collection bindObjects) { + return new SqlBuilder(conn, inList, sb, bindObjects); + } + + public static SqlBuilder of(final Connection conn) { + return new SqlBuilder(conn, defaultInList, new StringBuilder(), new ArrayList()); + } + + public static SqlBuilder of(final Connection conn, final Collection bindObjects) { + return new SqlBuilder(conn, defaultInList, new StringBuilder(), bindObjects); + } + + public static SqlBuilder of(final Connection conn, final StringBuilder sb) { + return new SqlBuilder(conn, defaultInList, sb, new ArrayList()); + } + + public static SqlBuilder of(final Connection conn, final InList inList) { + return new SqlBuilder(conn, inList, new StringBuilder(), new ArrayList()); + } + + // start custom SqlBuilder methods + + public SqlBuilder append(final String sql, final Object bindObject) { + sb.append(sql); + this.bindObjects.add(bindObject); + return this; + } + + public SqlBuilder append(final String sql, final Object... bindObjects) { + return this.append(sql, (Object) bindObjects); + } + + public SqlBuilder appendInList(final String columnName, final Collection values) throws SQLException { + final InList.InListObject inListObject = inList.inList(conn, columnName, values); + return this.append(inListObject.toString(), inListObject.getBindObject()); + } + + public SqlBuilder appendNotInList(final String columnName, final Collection values) throws SQLException { + final InList.InListObject inListObject = inList.notInList(conn, columnName, values); + return this.append(inListObject.toString(), inListObject.getBindObject()); + } + + public StringBuilder getStringBuilder() { + return sb; + } + + @Override + public Collection getBindObject() { + return bindObjects; + } + + public InList getInList() { + return inList; + } + + public Connection getConnection() { + return conn; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SqlBuilder)) return false; + + final SqlBuilder that = (SqlBuilder) o; + + return bindObjects.equals(that.bindObjects) && sb.toString().equals(that.sb.toString()); + } + + @Override + public int hashCode() { + int result = sb.toString().hashCode(); + result = 31 * result + bindObjects.hashCode(); + return result; + } + + // end custom SqlBuilder methods + // start StringBuilder delegates + + public SqlBuilder append(Object obj) { + sb.append(obj); + return this; + } + + public SqlBuilder append(String str) { + sb.append(str); + return this; + } + + public SqlBuilder append(StringBuffer sb) { + this.sb.append(sb); + return this; + } + + @Override + public SqlBuilder append(CharSequence s) { + sb.append(s); + return this; + } + + @Override + public SqlBuilder append(CharSequence s, int start, int end) { + sb.append(s, start, end); + return this; + } + + public SqlBuilder append(char[] str) { + sb.append(str); + return this; + } + + public SqlBuilder append(char[] str, int offset, int len) { + sb.append(str, offset, len); + return this; + } + + public SqlBuilder append(boolean b) { + sb.append(b); + return this; + } + + @Override + public SqlBuilder append(char c) { + sb.append(c); + return this; + } + + public SqlBuilder append(int i) { + sb.append(i); + return this; + } + + public SqlBuilder append(long lng) { + sb.append(lng); + return this; + } + + public SqlBuilder append(float f) { + sb.append(f); + return this; + } + + public SqlBuilder append(double d) { + sb.append(d); + return this; + } + + public SqlBuilder appendCodePoint(int codePoint) { + sb.appendCodePoint(codePoint); + return this; + } + + public SqlBuilder delete(int start, int end) { + sb.delete(start, end); + return this; + } + + public SqlBuilder deleteCharAt(int index) { + sb.deleteCharAt(index); + return this; + } + + public SqlBuilder replace(int start, int end, String str) { + sb.replace(start, end, str); + return this; + } + + public SqlBuilder insert(int index, char[] str, int offset, int len) { + sb.insert(index, str, offset, len); + return this; + } + + public SqlBuilder insert(int offset, Object obj) { + sb.insert(offset, obj); + return this; + } + + public SqlBuilder insert(int offset, String str) { + sb.insert(offset, str); + return this; + } + + public SqlBuilder insert(int offset, char[] str) { + sb.insert(offset, str); + return this; + } + + public SqlBuilder insert(int dstOffset, CharSequence s) { + sb.insert(dstOffset, s); + return this; + } + + public SqlBuilder insert(int dstOffset, CharSequence s, int start, int end) { + sb.insert(dstOffset, s, start, end); + return this; + } + + public SqlBuilder insert(int offset, boolean b) { + sb.insert(offset, b); + return this; + } + + public SqlBuilder insert(int offset, char c) { + sb.insert(offset, c); + return this; + } + + public SqlBuilder insert(int offset, int i) { + sb.insert(offset, i); + return this; + } + + public SqlBuilder insert(int offset, long l) { + sb.insert(offset, l); + return this; + } + + public SqlBuilder insert(int offset, float f) { + sb.insert(offset, f); + return this; + } + + public SqlBuilder insert(int offset, double d) { + sb.insert(offset, d); + return this; + } + + public int indexOf(String str) { + return sb.indexOf(str); + } + + public int indexOf(String str, int fromIndex) { + return sb.indexOf(str, fromIndex); + } + + public int lastIndexOf(String str) { + return sb.lastIndexOf(str); + } + + public int lastIndexOf(String str, int fromIndex) { + return sb.lastIndexOf(str, fromIndex); + } + + public SqlBuilder reverse() { + sb.reverse(); + return this; + } + + @Override + public String toString() { + return sb.toString(); + } + + @Override + public int length() { + return sb.length(); + } + + public int capacity() { + return sb.capacity(); + } + + public void ensureCapacity(int minimumCapacity) { + sb.ensureCapacity(minimumCapacity); + } + + public void trimToSize() { + sb.trimToSize(); + } + + public void setLength(int newLength) { + sb.setLength(newLength); + } + + @Override + public char charAt(int index) { + return sb.charAt(index); + } + + public int codePointAt(int index) { + return sb.codePointAt(index); + } + + public int codePointBefore(int index) { + return sb.codePointBefore(index); + } + + public int codePointCount(int beginIndex, int endIndex) { + return sb.codePointCount(beginIndex, endIndex); + } + + public int offsetByCodePoints(int index, int codePointOffset) { + return sb.offsetByCodePoints(index, codePointOffset); + } + + public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) { + sb.getChars(srcBegin, srcEnd, dst, dstBegin); + } + + public void setCharAt(int index, char ch) { + sb.setCharAt(index, ch); + } + + public String substring(int start) { + return sb.substring(start); + } + + @Override + public CharSequence subSequence(int start, int end) { + return sb.subSequence(start, end); + } + + public String substring(int start, int end) { + return sb.substring(start, end); + } + + //IFJAVA8_START + + @Override + public IntStream chars() { + return sb.chars(); + } + + @Override + public IntStream codePoints() { + return sb.codePoints(); + } + + //IFJAVA8_END + + // end StringBuilder delegates + // start Collection delegates + + @Override + public int size() { + return bindObjects.size(); + } + + @Override + public boolean isEmpty() { + return bindObjects.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return bindObjects.contains(o); + } + + @Override + public Iterator iterator() { + return bindObjects.iterator(); + } + + @Override + public Object[] toArray() { + return bindObjects.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return bindObjects.toArray(a); + } + + @Override + public boolean add(Object e) { + return bindObjects.add(e); + } + + @Override + public boolean remove(Object o) { + return bindObjects.remove(o); + } + + @Override + public boolean containsAll(Collection c) { + return bindObjects.containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + return bindObjects.addAll(c); + } + + @Override + public boolean removeAll(Collection c) { + return bindObjects.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) { + return bindObjects.retainAll(c); + } + + @Override + public void clear() { + bindObjects.clear(); + } + + //IFJAVA8_START + + @Override + public boolean removeIf(Predicate filter) { + return bindObjects.removeIf(filter); + } + + @Override + public Spliterator spliterator() { + return bindObjects.spliterator(); + } + + @Override + public Stream stream() { + return bindObjects.stream(); + } + + @Override + public Stream parallelStream() { + return bindObjects.parallelStream(); + } + + @Override + public void forEach(Consumer action) { + bindObjects.forEach(action); + } + + //IFJAVA8_END + + // end Collection delegates +} 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 f6e8f2e..1e2bc1e 100644 --- a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java +++ b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java @@ -3,6 +3,7 @@ package com.moparisthebest.jdbc.codegen; import com.moparisthebest.jdbc.*; import com.moparisthebest.jdbc.codegen.spring.SpringRepository; import com.moparisthebest.jdbc.codegen.spring.SpringScope; +import com.moparisthebest.jdbc.util.Bindable; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; @@ -28,6 +29,7 @@ import java.util.stream.Stream; import static com.moparisthebest.jdbc.TryClose.tryClose; import static com.moparisthebest.jdbc.codegen.JdbcMapper.DatabaseType.ORACLE; import static com.moparisthebest.jdbc.codegen.JdbcMapperFactory.SUFFIX; +import static com.moparisthebest.jdbc.codegen.SpecialVariableElement.SpecialType.SQL; /** * Created by mopar on 5/24/17. @@ -36,7 +38,7 @@ import static com.moparisthebest.jdbc.codegen.JdbcMapperFactory.SUFFIX; @SupportedOptions({"jdbcMapper.databaseType", "jdbcMapper.arrayNumberTypeName", "jdbcMapper.arrayStringTypeName", "jdbcMapper.allowedMaxRowParamNames", "jdbcMapper.sqlCheckerClass"}) public class JdbcMapperProcessor extends AbstractProcessor { - 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 Pattern paramPattern = Pattern.compile("\\{(([^\\s]+)\\s+(([Nn][Oo][Tt]\\s+)?[Ii][Nn]\\s+))?([BbCcSs][LlQq][OoLl][Bb]?\\s*:\\s*([^:}]+\\s*:\\s*)?)?([^}]+)\\}"); public static final SourceVersion RELEASE_8; public static boolean java8; @@ -64,11 +66,10 @@ public class JdbcMapperProcessor extends AbstractProcessor { } static TypeMirror sqlExceptionType, stringType, numberType, utilDateType, readerType, clobType, connectionType, jdbcMapperType, - byteArrayType, inputStreamType, fileType, blobType, sqlArrayType, collectionType, calendarType, cleanerType, enumType; + byteArrayType, inputStreamType, fileType, blobType, sqlArrayType, collectionType, iterableType, bindableType, calendarType, cleanerType, enumType; //IFJAVA8_START static TypeMirror streamType, instantType, localDateTimeType, localDateType, localTimeType, zonedDateTimeType, offsetDateTimeType, offsetTimeType; //IFJAVA8_END - private TypeElement cleanerElement; private JdbcMapper.DatabaseType defaultDatabaseType; private String defaultArrayNumberTypeName, defaultArrayStringTypeName; private SQLChecker sqlChecker; @@ -124,9 +125,10 @@ public class JdbcMapperProcessor extends AbstractProcessor { byteArrayType = types.getArrayType(types.getPrimitiveType(TypeKind.BYTE)); sqlArrayType = elements.getTypeElement(java.sql.Array.class.getCanonicalName()).asType(); collectionType = types.getDeclaredType(elements.getTypeElement(Collection.class.getCanonicalName()), types.getWildcardType(null, null)); + iterableType = types.getDeclaredType(elements.getTypeElement(Iterable.class.getCanonicalName()), types.getWildcardType(null, null)); - cleanerElement = elements.getTypeElement(Cleaner.class.getCanonicalName()); - cleanerType = types.getDeclaredType(cleanerElement, types.getWildcardType(null, null)); + bindableType = elements.getTypeElement(Bindable.class.getCanonicalName()).asType(); + cleanerType = types.getDeclaredType(elements.getTypeElement(Cleaner.class.getCanonicalName()), types.getWildcardType(null, null)); enumType = types.getDeclaredType(elements.getTypeElement(Enum.class.getCanonicalName()), types.getWildcardType(null, null)); @@ -344,6 +346,8 @@ public class JdbcMapperProcessor extends AbstractProcessor { String calendarName = null, cleanerName = null; CompileTimeResultSetMapper.MaxRows maxRows = CompileTimeResultSetMapper.MaxRows.getMaxRows(sql.maxRows()); boolean sqlExceptionThrown = false; + boolean sqlParam = false; + boolean sqlIterableParam = false; { // now parameters final List params = eeMethod.getParameters(); @@ -390,29 +394,40 @@ public class JdbcMapperProcessor extends AbstractProcessor { continue; } unusedParams.remove(paramName); - final String clobBlob = bindParamMatcher.group(5); + final String clobBlobSql = bindParamMatcher.group(5); final String inColumnName = bindParamMatcher.group(2); if (inColumnName == null) { - bindParamMatcher.appendReplacement(sb, "?"); - if(clobBlob == null){ + if(clobBlobSql == null){ + bindParamMatcher.appendReplacement(sb, "?"); 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)); + final String upperClobBlobSql = clobBlobSql.toUpperCase(); + String blobCharset = bindParamMatcher.group(6); + if(blobCharset != null) + blobCharset = blobCharset.substring(0, blobCharset.indexOf(':')).trim(); + if(upperClobBlobSql.startsWith("SQL")) { + bindParamMatcher.appendReplacement(sb, "REPLACEMEWITHUNQUOTEDQUOTEPLZ + " + paramName + " + REPLACEMEWITHUNQUOTEDQUOTEPLZ"); + final SpecialVariableElement sve = new SpecialVariableElement(bindParam, SQL, blobCharset); + bindParams.add(sve); + sqlParam = true; + sqlIterableParam |= sve.iterable || sve.bindable; + } else if(upperClobBlobSql.startsWith("CLOB") || upperClobBlobSql.startsWith("BLOB")) { + bindParamMatcher.appendReplacement(sb, "?"); + final boolean clobNotBlob = 'C' == upperClobBlobSql.charAt(0); + 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)); + } } else { - bindParams.add(new SpecialVariableElement(bindParam, SpecialVariableElement.SpecialType.BLOB, blobCharset == null ? null : - blobCharset.substring(0, blobCharset.indexOf(':')).trim() - )); + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "special variable type can only be clob/blob/sql, not " + clobBlobSql, bindParam); } } } else { - if(clobBlob != null) - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "cannot combine in/not in and clob/blob", bindParam); + if(clobBlobSql != null) + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "cannot combine in/not in and clob/blob/sql", bindParam); SpecialVariableElement inListBindParam = inListBindParams.get(paramName); if(inListBindParam == null) { inListBindParam = new SpecialVariableElement(bindParam, @@ -502,7 +517,7 @@ public class JdbcMapperProcessor extends AbstractProcessor { w.write("\t\t\tps = "); final boolean isGeneratedKeyLong = !parsedSQl.isSelect() && (returnType.equals("long") || returnType.equals("java.lang.Long")); // todo: make isGeneratedKeyLong work with cachePreparedStatements - final boolean cachePreparedStatements = sql.cachePreparedStatement().combine(defaultCachePreparedStatements) && !bindInList && !isGeneratedKeyLong; + final boolean cachePreparedStatements = sql.cachePreparedStatement().combine(defaultCachePreparedStatements) && !bindInList && !isGeneratedKeyLong && !sqlParam; if (cachePreparedStatements) { w.write("this.prepareStatement("); w.write(Integer.toString(cachedPreparedStatements)); @@ -527,14 +542,17 @@ public class JdbcMapperProcessor extends AbstractProcessor { w.write(");\n"); // now bind parameters - if(bindInList) { + if(bindInList || sqlIterableParam) { w.write("\t\t\tint psParamCount = 0;\n"); for (final VariableElement param : bindParams) setObject(w, "++psParamCount", param); } else { int count = 0; - for (final VariableElement param : bindParams) - setObject(w, Integer.toString(++count), param); + for (final VariableElement param : bindParams) { + // better place/way to do this? + if(!(param instanceof SpecialVariableElement && ((SpecialVariableElement)param).specialType == SQL)) + setObject(w, Integer.toString(++count), param); + } } boolean closeRs = true; @@ -922,6 +940,16 @@ public class JdbcMapperProcessor extends AbstractProcessor { } break; } + case SQL: { + if(specialParam.iterable || specialParam.bindable) { + w.append("psParamCount = com.moparisthebest.jdbc.util.PreparedStatementUtil.recursiveBindIndex(ps, psParamCount, ").append(specialParam.name); + if(specialParam.bindable) + w.append(".getBindObject()"); // handle null? I think no... because then the SQL query get's "null" put in it and meh... can return noBind or an empty array... + w.append(");"); + } + w.append('\n'); + 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 62e923f..58f4c6e 100644 --- a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SimpleSQLChecker.java +++ b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SimpleSQLChecker.java @@ -128,6 +128,8 @@ public class SimpleSQLChecker implements SQLChecker { return new ByteArrayInputStream(new byte[1]); case CLOB: return new StringReader(defaultString); + case SQL: + return specialParam.blobStringCharset == null ? "" : specialParam.blobStringCharset; } } // end special behavior diff --git a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SpecialVariableElement.java b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SpecialVariableElement.java index 97910df..95c2ca7 100644 --- a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SpecialVariableElement.java +++ b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/SpecialVariableElement.java @@ -16,12 +16,14 @@ class SpecialVariableElement implements VariableElement { IN_LIST, CLOB, BLOB, + SQL, } final VariableElement delegate; final SpecialType specialType; final String blobStringCharset; final int index; + final boolean iterable, bindable; String name, componentTypeString; @@ -43,6 +45,8 @@ class SpecialVariableElement implements VariableElement { this.blobStringCharset = blobStringCharset; this.index = index; this.name = getSimpleName().toString(); + this.iterable = specialType == SpecialType.SQL && JdbcMapperProcessor.types.isAssignable(delegate.asType(), JdbcMapperProcessor.iterableType); + this.bindable = !this.iterable && specialType == SpecialType.SQL && JdbcMapperProcessor.types.isAssignable(delegate.asType(), JdbcMapperProcessor.bindableType); } public String getName() { diff --git a/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/ParamPatternTest.java b/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/ParamPatternTest.java index ea147df..1593af6 100644 --- a/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/ParamPatternTest.java +++ b/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/ParamPatternTest.java @@ -30,6 +30,14 @@ public class ParamPatternTest { assertEquals("utf-16", blobCharset.substring(0, blobCharset.indexOf(':')).trim()); blobCharset = "utf-16 : "; assertEquals("utf-16", blobCharset.substring(0, blobCharset.indexOf(':')).trim()); + + testMatch("{sql:sqlStatement}", s(null, null, null, null, "sql:", null, "sqlStatement")); + testMatch("{sql: sqlStatement}", s(null, null, null, null, "sql: ", null, "sqlStatement")); + testMatch("{SQL: sqlStatement}", s(null, null, null, null, "SQL: ", null, "sqlStatement")); + testMatch("{Sql: sqlStatement}", s(null, null, null, null, "Sql: ", null, "sqlStatement")); + testMatch("{Sql : sqlStatement}", s(null, null, null, null, "Sql : ", null, "sqlStatement")); + testMatch("{sql:person:sqlStatement}", s(null, null, null, null, "sql:person:", "person:", "sqlStatement")); + testMatch("{sql:JOIN person ON p.person_no = b.person_no:sqlStatement}", s(null, null, null, null, "sql:JOIN person ON p.person_no = b.person_no:", "JOIN person ON p.person_no = b.person_no:", "sqlStatement")); } private static void testMatch(final String sql, final Collection expected) { diff --git a/querymapper/src/main/java/com/moparisthebest/jdbc/InList.java b/querymapper/src/main/java/com/moparisthebest/jdbc/InList.java deleted file mode 100644 index 430ee5c..0000000 --- a/querymapper/src/main/java/com/moparisthebest/jdbc/InList.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.moparisthebest.jdbc; - -import com.moparisthebest.jdbc.util.InListUtil; - -import java.sql.Connection; -import java.sql.SQLException; -import java.util.Collection; - -/** - * For a column name and a Collection, return a Object usable by QueryMapper for binding to a PreparedStatement and - * ListQueryMapper for substituting in the query - */ -public interface InList { - - /** - * Returns an InList instance for use with this connection - * @param conn connection which may be inspected to determine best InList to use - * @return InList instance - */ - public InList instance(final Connection conn); - - /** - * Returns an Object who's .toString returns a String for a query, and QueryMapper knows how to bind to a PreparedStatement - * @param columnName Column name for query - * @param values values for in list - * @return object - */ - public InListObject inList(final Connection conn, final String columnName, final Collection values) throws SQLException; - - /** - * Returns an Object who's .toString returns a String for a query, and QueryMapper knows how to bind to a PreparedStatement - * @param columnName Column name for query - * @param values values for not in list - * @return object - */ - public InListObject notInList(final Connection conn, final String columnName, final Collection values) throws SQLException; - - class InListObject { - static final InListObject inEmpty = new InListObject(InListUtil.inEmpty); - static final InListObject notInEmpty = new InListObject(InListUtil.notInEmpty); - - private final String sql; - - public InListObject(final String sql) { - this.sql = sql; - } - - @Override - public final String toString() { - return sql; - } - } -} diff --git a/querymapper/src/main/java/com/moparisthebest/jdbc/ListQueryMapper.java b/querymapper/src/main/java/com/moparisthebest/jdbc/ListQueryMapper.java index 3b5effc..d410a37 100644 --- a/querymapper/src/main/java/com/moparisthebest/jdbc/ListQueryMapper.java +++ b/querymapper/src/main/java/com/moparisthebest/jdbc/ListQueryMapper.java @@ -1,17 +1,9 @@ package com.moparisthebest.jdbc; -import com.moparisthebest.jdbc.codegen.JdbcMapper; -import com.moparisthebest.jdbc.util.ResultSetIterable; - -import java.lang.reflect.Method; import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; -import java.util.*; -//IFJAVA8_START -import java.util.stream.Stream; -//IFJAVA8_END + +import static com.moparisthebest.jdbc.InList.defaultInList; public class ListQueryMapper extends QueryMapper { diff --git a/querymapper/src/main/java/com/moparisthebest/jdbc/QueryMapper.java b/querymapper/src/main/java/com/moparisthebest/jdbc/QueryMapper.java index 52e7f5a..20f9720 100644 --- a/querymapper/src/main/java/com/moparisthebest/jdbc/QueryMapper.java +++ b/querymapper/src/main/java/com/moparisthebest/jdbc/QueryMapper.java @@ -2,8 +2,10 @@ package com.moparisthebest.jdbc; import com.moparisthebest.jdbc.codegen.JdbcMapper; import com.moparisthebest.jdbc.codegen.JdbcMapperFactory; +import com.moparisthebest.jdbc.util.PreparedStatementUtil; import com.moparisthebest.jdbc.util.ResultSetIterable; import com.moparisthebest.jdbc.util.ResultSetUtil; +import com.moparisthebest.jdbc.util.SqlBuilder; import java.io.*; import java.lang.reflect.Method; @@ -13,15 +15,14 @@ import java.util.*; //IFJAVA8_START import java.time.*; import java.util.stream.Stream; - -import static java.nio.charset.StandardCharsets.UTF_8; //IFJAVA8_END +import static com.moparisthebest.jdbc.InList.defaultInList; import static com.moparisthebest.jdbc.TryClose.tryClose; public class QueryMapper implements JdbcMapper { - public static final Object noBind = new Object(); + public static final Object noBind = PreparedStatementUtil.noBind; public static final ResultSetMapper defaultRsm = new ResultSetMapper(); protected static final int[] ORACLE_SINGLE_COLUMN_INDEX = new int[]{1}; @@ -51,46 +52,8 @@ public class QueryMapper implements JdbcMapper { return conn.prepareStatement(sql); } }; - - private static final Charset UTF_8 = Charset.forName("UTF-8"); IFJAVA6_END*/ - protected static final InList defaultInList = getDefaultInList(); - - private static InList getDefaultInList() { - try { - final String inListClassName = System.getProperty("QueryMapper.defaultInList.class"); - if(inListClassName != null) { - final Class inListClass = Class.forName(inListClassName); - final Method method = inListClass.getMethod(System.getProperty("QueryMapper.defaultInList.method", "instance")); - return (InList) method.invoke(null); - } else { - // todo: change default to OPTIMAL ? - final String type = System.getProperty("queryMapper.databaseType", System.getProperty("jdbcMapper.databaseType", "BIND")); - if(type.equals("OPTIMAL")) { - return OptimalInList.instance(); - } else { - switch (JdbcMapper.DatabaseType.valueOf(type)) { - case DEFAULT: - case BIND: - return BindInList.instance(); - case ANY: - return ArrayInList.instance(); - case ORACLE: - return OracleArrayInList.instance(); - case UNNEST: - return UnNestArrayInList.instance(); - default: - throw new RuntimeException("Invalid queryMapper.databaseType: " + type); - } - } - } - } catch (Throwable e) { - // NEVER ignore - throw new RuntimeException(e); - } - } - protected final ResultSetMapper cm; protected final Connection conn; protected final boolean closeConn; @@ -167,53 +130,16 @@ public class QueryMapper implements JdbcMapper { } } - private static class StringWrapper { - public final String s; - - private StringWrapper(String s) { - this.s = s; - } - - public String toString() { - return s; - } - - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof StringWrapper)) return false; - StringWrapper that = (StringWrapper) o; - return !(s != null ? !s.equals(that.s) : that.s != null); - } - - public int hashCode() { - return s != null ? s.hashCode() : 0; - } - } - - private static class ClobString extends StringWrapper { - private ClobString(String s) { - super(s); - } - } - - private static class BlobString extends StringWrapper { - private final Charset charset; - private BlobString(final String s, final Charset charset) { - super(s); - this.charset = charset; - } - } - public static Object wrapClob(String s) { - return new ClobString(s); + return PreparedStatementUtil.wrapClob(s); } public static Object wrapBlob(String s) { - return new BlobString(s, UTF_8); + return PreparedStatementUtil.wrapBlob(s); } public static Object wrapBlob(final String s, final Charset charset) { - return new BlobString(s, charset == null ? UTF_8 : charset); + return PreparedStatementUtil.wrapBlob(s, charset); } // start in list specific code @@ -238,6 +164,10 @@ public class QueryMapper implements JdbcMapper { recursiveReplace(sb, (Object[]) o); } else if (o instanceof Collection) { recursiveReplace(sb, ((Collection) o).toArray()); + } else if(o instanceof Iterable) { + for(final Object o2 : (Iterable) o) { + recursiveReplace(sb, o2); + } } } } @@ -262,142 +192,25 @@ public class QueryMapper implements JdbcMapper { // end in list specific code public static void setObject(final PreparedStatement ps, final int index, final Object o) throws SQLException { - // we are going to put most common ones up top so it should execute faster normally - if (o == null || o instanceof String || o instanceof Number) - ps.setObject(index, o); - // java.util.Date support, put it in a Timestamp - else if (o instanceof java.util.Date) - ps.setObject(index, o.getClass().equals(java.util.Date.class) ? new java.sql.Timestamp(((java.util.Date)o).getTime()) : o); - //IFJAVA8_START// todo: other java.time types - else if (o instanceof Instant) - ps.setObject(index, java.sql.Timestamp.from((Instant)o)); - else if (o instanceof LocalDateTime) - ps.setObject(index, java.sql.Timestamp.valueOf((LocalDateTime)o)); - else if (o instanceof LocalDate) - ps.setObject(index, java.sql.Date.valueOf((LocalDate)o)); - else if (o instanceof LocalTime) - ps.setObject(index, java.sql.Time.valueOf((LocalTime)o)); - else if (o instanceof ZonedDateTime) - ps.setObject(index, java.sql.Timestamp.from(((ZonedDateTime)o).toInstant())); - else if (o instanceof OffsetDateTime) - ps.setObject(index, java.sql.Timestamp.from(((OffsetDateTime)o).toInstant())); - else if (o instanceof OffsetTime) - ps.setObject(index, java.sql.Time.valueOf(((OffsetTime)o).toLocalTime())); // todo: no timezone? - - //IFJAVA8_END - // CLOB support - else if (o instanceof Reader) - ps.setClob(index, (Reader) o); - else if (o instanceof ClobString) - ps.setClob(index, ((ClobString) o).s == null ? null : new StringReader(((ClobString) o).s)); - else if (o instanceof java.sql.Clob) - ps.setClob(index, (java.sql.Clob) o); - // BLOB support - else if (o instanceof byte[]) - ps.setBlob(index, new ByteArrayInputStream((byte[]) o)); - else if (o instanceof InputStream) - ps.setBlob(index, (InputStream) o); - else if (o instanceof File) - try { - ps.setBlob(index, new FileInputStream((File) o)); // todo: does this close this or leak a file descriptor? - } catch (FileNotFoundException e) { - throw new SQLException("File to Blob FileNotFoundException", e); - } - else if (o instanceof BlobString) - ps.setBlob(index, ((BlobString) o).s == null ? null : new ByteArrayInputStream(((BlobString) o).s.getBytes(((BlobString) o).charset))); - else if (o instanceof java.sql.Blob) - ps.setBlob(index, (java.sql.Blob) o); - else if (o instanceof ArrayInList.ArrayListObject) - ps.setArray(index, ((ArrayInList.ArrayListObject) o).getArray()); - else if (o instanceof java.sql.Array) - ps.setArray(index, (java.sql.Array) o); - else if (o instanceof Enum) - ps.setObject(index, ((Enum)o).name()); - else - ps.setObject(index, o); // probably won't get here ever, but just in case... - /* - switch(ps.getParameterMetaData().getParameterType(index)){ // 'java.sql.SQLException: Unsupported feature', fully JDBC 3.0 compliant my ass, freaking oracle... - case Types.CLOB: - if(o instanceof String) - ps.setObject(index, o); - else if (o instanceof Reader) - ps.setClob(index, (Reader) o); - else if (o instanceof Clob) - ps.setClob(index, (Clob) o); - return; - case Types.BLOB: - if (o instanceof byte[]) - ps.setBlob(index, new ByteArrayInputStream((byte[])o)); - else if (o instanceof InputStream) - ps.setBlob(index, (InputStream) o); - else if (o instanceof File) - try { - ps.setBlob(index, new FileInputStream((File) o)); - } catch (FileNotFoundException e) { - throw new SQLException("File to Blob FileNotFoundException", e); - } - else if (o instanceof Blob) - ps.setBlob(index, (Blob) o); - else if(o instanceof String) - try{ - ps.setBlob(index, new ByteArrayInputStream(((String) o).getBytes("UTF-8"))); - }catch(UnsupportedEncodingException e){ - throw new SQLException("String to Blob UnsupportedEncodingException", e); - } - return; - default: - ps.setObject(index, o); - } - */ + PreparedStatementUtil.setObject(ps, index, o); } public static int recursiveBind(final PreparedStatement ps, final Object... bindObjects) throws SQLException { - return recursiveBind(ps, 0, bindObjects); - } - - private static int recursiveBind(final PreparedStatement ps, int index, final Object... bindObjects) throws SQLException { - if (bindObjects != null && bindObjects.length > 0) { - for (Object o : bindObjects) { - if (o != null) { - if (o == InList.InListObject.inEmpty || o == InList.InListObject.notInEmpty || o == noBind) { - continue; // ignore - } else if (o instanceof BindInList.BindInListObject) { - if (((BindInList.BindInListObject) o).getBindObjects() != null) - index = recursiveBind(ps, index, ((BindInList.BindInListObject) o).getBindObjects()); - continue; - } else if (o instanceof Object[]) { - index = recursiveBind(ps, index, (Object[]) o); - continue; - } else if (o instanceof Collection) { - index = recursiveBind(ps, index, ((Collection) o).toArray()); - continue; - } - } - //System.out.printf("index: '%d' bound to '%s'\n", index+1, o); - setObject(ps, ++index, o); - //ps.setObject(++index, o); - } - } - return index; + return PreparedStatementUtil.recursiveBind(ps, bindObjects); } public static PreparedStatement bindStatement(final PreparedStatement ps, final Object... bindObjects) throws SQLException { - recursiveBind(ps, bindObjects); - return ps; - } - - protected static PreparedStatement bind(final PreparedStatement ps, final Object... bindObjects) throws SQLException { - return bindStatement(ps, bindObjects); + return PreparedStatementUtil.bind(ps, bindObjects); } protected static ResultSet bindExecute(final PreparedStatement ps, final Object... bindObjects) throws SQLException { - return bind(ps, bindObjects).executeQuery(); + return PreparedStatementUtil.bind(ps, bindObjects).executeQuery(); } // these update the database public int executeUpdate(PreparedStatement ps, final Object... bindObjects) throws SQLException { - return bind(ps, bindObjects).executeUpdate(); + return PreparedStatementUtil.bind(ps, bindObjects).executeUpdate(); } public boolean executeUpdateSuccess(PreparedStatement ps, final Object... bindObjects) throws SQLException { diff --git a/test/src/main/java/com/moparisthebest/jdbc/codegen/QmDao.java b/test/src/main/java/com/moparisthebest/jdbc/codegen/QmDao.java index acfac2d..ea61ae5 100644 --- a/test/src/main/java/com/moparisthebest/jdbc/codegen/QmDao.java +++ b/test/src/main/java/com/moparisthebest/jdbc/codegen/QmDao.java @@ -1,10 +1,10 @@ package com.moparisthebest.jdbc.codegen; import com.moparisthebest.jdbc.dto.*; -import com.moparisthebest.jdbc.util.CaseInsensitiveHashMap; -import com.moparisthebest.jdbc.util.ResultSetIterable; +import com.moparisthebest.jdbc.util.*; import java.sql.SQLException; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -264,4 +264,23 @@ public interface QmDao extends JdbcMapper { @SQL("INSERT INTO a_thaoeu_table (a_thaoeu_table_val) VALUES ({value})") long insertGetGeneratedKey(long value) throws SQLException; + + @SQL("SELECT person_no FROM person WHERE {sql:sql}") + List selectRandomSql(String sql) throws SQLException; + + @SQL("SELECT person_no FROM person WHERE {sql:sql}") + List selectRandomSqlBuilder(SqlBuilder sql) throws SQLException; + + @SQL("SELECT person_no FROM person WHERE person_no = {personNo1} {sql:sql} OR first_name = {firstName}") + List selectRandomSql(long personNo1, String sql, String firstName) throws SQLException; + + @SQL("SELECT person_no FROM person WHERE person_no = {personNo1} {sql:sql} OR first_name = {firstName}") + List selectRandomSqlBuilder(long personNo1, Bindable sql, String firstName) throws SQLException; + + // these we just check if they generated + @SQL("INSERT {sql:sql}") + void insertRandomSqlCollection(Collection sql) throws SQLException; + + @SQL("INSERT {sql:sql}") + void insertRandomSqlIterable(Iterable sql) throws SQLException; } diff --git a/test/src/main/java/com/moparisthebest/jdbc/codegen/QueryMapperQmDao.java b/test/src/main/java/com/moparisthebest/jdbc/codegen/QueryMapperQmDao.java index 41abe14..a26e3b8 100644 --- a/test/src/main/java/com/moparisthebest/jdbc/codegen/QueryMapperQmDao.java +++ b/test/src/main/java/com/moparisthebest/jdbc/codegen/QueryMapperQmDao.java @@ -2,8 +2,7 @@ package com.moparisthebest.jdbc.codegen; import com.moparisthebest.jdbc.*; import com.moparisthebest.jdbc.dto.*; -import com.moparisthebest.jdbc.util.CaseInsensitiveHashMap; -import com.moparisthebest.jdbc.util.ResultSetIterable; +import com.moparisthebest.jdbc.util.*; import java.sql.Connection; import java.sql.SQLException; @@ -392,11 +391,11 @@ public class QueryMapperQmDao implements QmDao { @Override public List getFieldPeopleByName(final List personNos, final List names) throws SQLException { return qm.toList("SELECT * from person WHERE " + inListReplace + " AND (" + inListReplace + " OR " + inListReplace + ") ORDER BY person_no", - FieldPerson.class, - qm.inList("person_no", personNos), - qm.inList("first_name", names), - qm.inList("last_name", names) - ); + FieldPerson.class, + qm.inList("person_no", personNos), + qm.inList("first_name", names), + qm.inList("last_name", names) + ); } @Override @@ -413,4 +412,34 @@ public class QueryMapperQmDao implements QmDao { public long insertGetGeneratedKey(long value) throws SQLException { return qm.insertGetGeneratedKey("INSERT INTO a_thaoeu_table (a_thaoeu_table_val) VALUES (?)", value); } + + @Override + public List selectRandomSql(final String sql) throws SQLException { + return qm.toList("SELECT person_no FROM person WHERE " + sql, Long.class); + } + + @Override + public List selectRandomSqlBuilder(final SqlBuilder sql) throws SQLException { + return qm.toList("SELECT person_no FROM person WHERE " + sql, Long.class, sql); + } + + @Override + public List selectRandomSql(final long personNo1, final String sql, final String firstName) throws SQLException { + return qm.toList("SELECT person_no FROM person WHERE person_no = ? " + sql + " OR first_name = ?", Long.class, personNo1, firstName); + } + + @Override + public List selectRandomSqlBuilder(final long personNo1, final Bindable sql, final String firstName) throws SQLException { + return qm.toList("SELECT person_no FROM person WHERE person_no = ? " + sql + " OR first_name = ?", Long.class, personNo1, sql, firstName); + } + + @Override + public void insertRandomSqlCollection(final Collection sql) throws SQLException { + qm.executeUpdate("INSERT " + sql, sql); + } + + @Override + public void insertRandomSqlIterable(final Iterable sql) throws SQLException { + qm.executeUpdate("INSERT " + sql, sql); + } } diff --git a/test/src/main/java/com/moparisthebest/jdbc/codegen/QueryMapperTypeQmDao.java b/test/src/main/java/com/moparisthebest/jdbc/codegen/QueryMapperTypeQmDao.java index d1b11b0..e9045d2 100644 --- a/test/src/main/java/com/moparisthebest/jdbc/codegen/QueryMapperTypeQmDao.java +++ b/test/src/main/java/com/moparisthebest/jdbc/codegen/QueryMapperTypeQmDao.java @@ -3,8 +3,7 @@ package com.moparisthebest.jdbc.codegen; import com.moparisthebest.jdbc.ResultSetMapper; import com.moparisthebest.jdbc.TypeReference; import com.moparisthebest.jdbc.dto.*; -import com.moparisthebest.jdbc.util.CaseInsensitiveHashMap; -import com.moparisthebest.jdbc.util.ResultSetIterable; +import com.moparisthebest.jdbc.util.*; import java.sql.Connection; import java.sql.SQLException; @@ -358,4 +357,24 @@ public class QueryMapperTypeQmDao extends QueryMapperQmDao { public List getFieldPeopleNotIn(final List personNos) throws SQLException { return qm.toType("SELECT * from person WHERE " + inListReplace + " ORDER BY person_no", new TypeReference>() {}, qm.notInList("person_no", personNos)); } + + @Override + public List selectRandomSql(final String sql) throws SQLException { + return qm.toType("SELECT person_no FROM person WHERE " + sql, new TypeReference>() {}); + } + + @Override + public List selectRandomSqlBuilder(final SqlBuilder sql) throws SQLException { + return qm.toType("SELECT person_no FROM person WHERE " + sql, new TypeReference>() {}, sql); + } + + @Override + public List selectRandomSql(final long personNo1, final String sql, final String firstName) throws SQLException { + return qm.toType("SELECT person_no FROM person WHERE person_no = ? " + sql + " OR first_name = ?", new TypeReference>() {}, personNo1, firstName); + } + + @Override + public List selectRandomSqlBuilder(final long personNo1, final Bindable sql, final String firstName) throws SQLException { + return qm.toType("SELECT person_no FROM person WHERE person_no = ? " + sql + " OR first_name = ?", new TypeReference>() {}, personNo1, sql, firstName); + } } diff --git a/test/src/test/java/com/moparisthebest/jdbc/QueryMapperTest.java b/test/src/test/java/com/moparisthebest/jdbc/QueryMapperTest.java index 9b1d54f..3518ee2 100644 --- a/test/src/test/java/com/moparisthebest/jdbc/QueryMapperTest.java +++ b/test/src/test/java/com/moparisthebest/jdbc/QueryMapperTest.java @@ -6,7 +6,9 @@ import com.moparisthebest.jdbc.codegen.QmDao; import com.moparisthebest.jdbc.codegen.QueryMapperQmDao; import com.moparisthebest.jdbc.codegen.QueryMapperTypeQmDao; import com.moparisthebest.jdbc.dto.*; +import com.moparisthebest.jdbc.util.Bindable; import com.moparisthebest.jdbc.util.ResultSetIterable; +import com.moparisthebest.jdbc.util.SqlBuilder; import org.junit.*; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -756,4 +758,24 @@ public class QueryMapperTest { final List fromDb = qm.getFieldPeopleNotIn(Arrays.asList(bosses[0].getPersonNo(), bosses[1].getPersonNo(), bosses[2].getPersonNo())); assertArrayEquals(people, fromDb.toArray()); } + + @Test + public void testSelectRandomSql() throws Throwable { + final List arr = Arrays.asList(1L, 2L, 3L); + assertEquals(arr, qm.selectRandomSql("person_no in (1,2,3)")); + assertEquals(arr, qm.selectRandomSql(1L, " OR person_no in (2,3)", "NoNameMatch")); + assertEquals(Collections.singletonList(2L), qm.selectRandomSql(2L, "", "NoNameMatch")); + } + + @Test + public void testSelectRandomSqlBuilder() throws Throwable { + final List arr = Arrays.asList(1L, 2L, 3L); + assertEquals(arr, qm.selectRandomSqlBuilder(SqlBuilder.of(qm.getConnection()).appendInList("person_no", arr))); + assertEquals(arr, qm.selectRandomSqlBuilder(SqlBuilder.of(qm.getConnection()).append("person_no = ? OR ", 1L).appendInList("person_no", Arrays.asList(2L, 3L)))); + assertEquals(arr, qm.selectRandomSqlBuilder(SqlBuilder.of(qm.getConnection()).append("person_no = 1 OR ").appendInList("person_no", Arrays.asList(2L, 3L)))); + assertEquals(arr, qm.selectRandomSqlBuilder(1L, SqlBuilder.of(qm.getConnection()).append(" OR person_no in (2,3)"), "NoNameMatch")); + assertEquals(Collections.singletonList(2L), qm.selectRandomSqlBuilder(2L, SqlBuilder.of(qm.getConnection()), "NoNameMatch")); + assertEquals(Collections.singletonList(3L), qm.selectRandomSqlBuilder(3L, Bindable.empty, "NoNameMatch")); + assertEquals(arr, qm.selectRandomSqlBuilder(2L, SqlBuilder.of(qm.getConnection()).append("OR person_no = ? OR ", 1L).appendInList("person_no", Collections.singletonList(3L)), "NoNameMatch")); + } }