/* * Copyright 2004-2011 H2 Group. * Copyright 2011 James Moger. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.iciql; import java.lang.reflect.Field; import java.sql.Blob; import java.sql.Clob; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import com.iciql.Iciql.EnumType; import com.iciql.NestedConditions.And; import com.iciql.NestedConditions.Or; import com.iciql.bytecode.ClassReader; import com.iciql.util.IciqlLogger; import com.iciql.util.JdbcUtils; import com.iciql.util.Utils; /** * This class represents a query. * * @param * the return type */ public class Query { private Db db; private SelectTable from; private ArrayList conditions = Utils.newArrayList(); private ArrayList updateColumnDeclarations = Utils.newArrayList(); private int conditionDepth = 0; private ArrayList> joins = Utils.newArrayList(); private final IdentityHashMap> aliasMap = Utils.newIdentityHashMap(); private ArrayList> orderByList = Utils.newArrayList(); private ArrayList groupByExpressions = Utils.newArrayList(); private long limit; private long offset; private Query(Db db) { this.db = db; } /** * from() is a static factory method to build a Query object. * * @param db * @param alias * @return a query object */ @SuppressWarnings("unchecked") static Query from(Db db, T alias) { Query query = new Query(db); TableDefinition def = (TableDefinition) db.define(alias.getClass()); query.from = new SelectTable(db, query, alias, false); def.initSelectObject(query.from, alias, query.aliasMap, false); return query; } @SuppressWarnings("unchecked") static Query rebuild(Db db, T alias) { Query query = new Query(db); TableDefinition def = (TableDefinition) db.define(alias.getClass()); query.from = new SelectTable(db, query, alias, false); def.initSelectObject(query.from, alias, query.aliasMap, true); return query; } public long selectCount() { SQLStatement stat = getSelectStatement(false); stat.appendSQL("COUNT(*) "); appendFromWhere(stat); ResultSet rs = stat.executeQuery(); try { rs.next(); long value = rs.getLong(1); return value; } catch (SQLException e) { throw IciqlException.fromSQL(stat.getSQL(), e); } finally { JdbcUtils.closeSilently(rs, true); } } public List select() { return select(false); } public T selectFirst() { List list = limit(1).select(false); return list.isEmpty() ? null : list.get(0); } public List selectDistinct() { return select(true); } @SuppressWarnings("unchecked") public X selectFirst(Z x) { List list = limit(1).select(x); return list.isEmpty() ? null : list.get(0); } public void createView(Class viewClass) { TableDefinition viewDef = db.define(viewClass); SQLStatement fromWhere = new SQLStatement(db); appendFromWhere(fromWhere, false); SQLStatement stat = new SQLStatement(db); db.getDialect().prepareCreateView(stat, viewDef, fromWhere.toSQL()); IciqlLogger.create(stat.toSQL()); stat.execute(); } public void replaceView(Class viewClass) { db.dropView(viewClass); createView(viewClass); } public String getSQL() { SQLStatement stat = getSelectStatement(false); stat.appendSQL("*"); appendFromWhere(stat); return stat.getSQL().trim(); } /** * toSQL returns a static string version of the query with runtime variables * properly encoded. This method is also useful when combined with the where * clause methods like isParameter() or atLeastParameter() which allows * iciql to generate re-usable parameterized string statements. * * @return the sql query as plain text */ public String toSQL() { return toSQL(false); } /** * toSQL returns a static string version of the query with runtime variables * properly encoded. This method is also useful when combined with the where * clause methods like isParameter() or atLeastParameter() which allows * iciql to generate re-usable parameterized string statements. * * @param distinct * if true SELECT DISTINCT is used for the query * @return the sql query as plain text */ public String toSQL(boolean distinct) { return toSQL(distinct, null); } /** * toSQL returns a static string version of the query with runtime variables * properly encoded. This method is also useful when combined with the where * clause methods like isParameter() or atLeastParameter() which allows * iciql to generate re-usable parameterized string statements. * * @param distinct * if true SELECT DISTINCT is used for the query * @param k * k is used to select only the columns of the specified alias * for an inner join statement. An example of a generated * statement is: SELECT DISTINCT t1.* FROM sometable AS t1 INNER * JOIN othertable AS t2 ON t1.id = t2.id WHERE t2.flag = true * without the alias parameter the statement would start with * SELECT DISTINCT * FROM... * @return the sql query as plain text */ public String toSQL(boolean distinct, K k) { SQLStatement stat = new SQLStatement(getDb()); if (updateColumnDeclarations.size() > 0) { stat.appendSQL("UPDATE "); from.appendSQL(stat); stat.appendSQL(" SET "); int i = 0; for (UpdateColumn declaration : updateColumnDeclarations) { if (i++ > 0) { stat.appendSQL(", "); } declaration.appendSQL(stat); } appendWhere(stat); } else { stat.appendSQL("SELECT "); if (distinct) { stat.appendSQL("DISTINCT "); } if (k != null) { SelectTable sel = getSelectTable(k); if (sel == null) { // unknown alias, use wildcard IciqlLogger.warn("Alias {0} is not defined in the statement!", k.getClass()); stat.appendSQL("*"); } else if (isJoin()) { // join query, use AS alias String as = sel.getAs(); stat.appendSQL(as + ".*"); } else { // schema.table.* String schema = sel.getAliasDefinition().schemaName; String table = sel.getAliasDefinition().tableName; String as = getDb().getDialect().prepareTableName(schema, table); stat.appendSQL(as + ".*"); } } else { // alias unspecified, use wildcard stat.appendSQL("*"); } appendFromWhere(stat); } return stat.toSQL().trim(); } String toSubQuery(Z z) { SQLStatement stat = getSelectStatement(false); SelectColumn col = aliasMap.get(z); String columnName = col.getFieldDefinition().columnName; stat.appendColumn(columnName); appendFromWhere(stat); return stat.toSQL(); } private List select(boolean distinct) { List result = Utils.newArrayList(); TableDefinition def = from.getAliasDefinition(); SQLStatement stat = getSelectStatement(distinct); def.appendSelectList(stat); appendFromWhere(stat); ResultSet rs = stat.executeQuery(); try { int[] columns = def.mapColumns(false, rs); while (rs.next()) { T item = from.newObject(); def.readRow(db.getDialect(), item, rs, columns); result.add(item); } } catch (SQLException e) { throw IciqlException.fromSQL(stat.getSQL(), e); } finally { JdbcUtils.closeSilently(rs, true); } return result; } public int delete() { SQLStatement stat = new SQLStatement(db); stat.appendSQL("DELETE FROM "); from.appendSQL(stat); appendWhere(stat); IciqlLogger.delete(stat.getSQL()); return stat.executeUpdate(); } public UpdateColumnSet set(A field) { from.getAliasDefinition().checkMultipleEnums(field); return new UpdateColumnSet(this, field); } public UpdateColumnSet set(boolean field) { from.getAliasDefinition().checkMultipleBooleans(); return setPrimitive(field); } public UpdateColumnSet set(byte field) { return setPrimitive(field); } public UpdateColumnSet set(short field) { return setPrimitive(field); } public UpdateColumnSet set(int field) { return setPrimitive(field); } public UpdateColumnSet set(long field) { return setPrimitive(field); } public UpdateColumnSet set(float field) { return setPrimitive(field); } public UpdateColumnSet set(double field) { return setPrimitive(field); } private UpdateColumnSet setPrimitive(A field) { A alias = getPrimitiveAliasByValue(field); if (alias == null) { // this will result in an unmapped field exception return set(field); } return set(alias); } public UpdateColumnIncrement increment(A field) { return new UpdateColumnIncrement(this, field); } public UpdateColumnIncrement increment(byte field) { return incrementPrimitive(field); } public UpdateColumnIncrement increment(short field) { return incrementPrimitive(field); } public UpdateColumnIncrement increment(int field) { return incrementPrimitive(field); } public UpdateColumnIncrement increment(long field) { return incrementPrimitive(field); } public UpdateColumnIncrement increment(float field) { return incrementPrimitive(field); } public UpdateColumnIncrement increment(double field) { return incrementPrimitive(field); } private UpdateColumnIncrement incrementPrimitive(A field) { A alias = getPrimitiveAliasByValue(field); if (alias == null) { // this will result in an unmapped field exception return increment(field); } return increment(alias); } public int update() { if (updateColumnDeclarations.size() == 0) { throw new IciqlException("Missing set or increment call."); } SQLStatement stat = new SQLStatement(db); stat.appendSQL("UPDATE "); from.appendSQL(stat); stat.appendSQL(" SET "); int i = 0; for (UpdateColumn declaration : updateColumnDeclarations) { if (i++ > 0) { stat.appendSQL(", "); } declaration.appendSQL(stat); } appendWhere(stat); IciqlLogger.update(stat.getSQL()); return stat.executeUpdate(); } public List selectDistinct(Z x) { return select(x, true); } public List select(Z x) { return select(x, false); } @SuppressWarnings("unchecked") private List select(Z x, boolean distinct) { Class clazz = x.getClass(); if (Utils.isSimpleType(clazz)) { return selectSimple((X) x, distinct); } Class enclosingClass = clazz.getEnclosingClass(); if (enclosingClass != null) { // anonymous inner class clazz = clazz.getSuperclass(); } return select((Class) clazz, (X) x, distinct); } private List select(Class clazz, X x, boolean distinct) { List result = Utils.newArrayList(); TableDefinition def = db.define(clazz); SQLStatement stat = getSelectStatement(distinct); def.appendSelectList(stat, this, x); appendFromWhere(stat); ResultSet rs = stat.executeQuery(); try { int[] columns = def.mapColumns(false, rs); while (rs.next()) { X row = Utils.newObject(clazz); def.readRow(db.getDialect(), row, rs, columns); result.add(row); } } catch (SQLException e) { throw IciqlException.fromSQL(stat.getSQL(), e); } finally { JdbcUtils.closeSilently(rs, true); } return result; } @SuppressWarnings("unchecked") private List selectSimple(X x, boolean distinct) { SQLStatement stat = getSelectStatement(distinct); appendSQL(stat, null, x); appendFromWhere(stat); ResultSet rs = stat.executeQuery(); List result = Utils.newArrayList(); try { while (rs.next()) { X value; Object o = rs.getObject(1); // Convert CLOB and BLOB now because we close the resultset if (Clob.class.isAssignableFrom(o.getClass())) { value = (X) Utils.convert(o, String.class); } else if (Blob.class.isAssignableFrom(o.getClass())) { value = (X) Utils.convert(o, byte[].class); } else { value = (X) o; } result.add(value); } } catch (Exception e) { throw IciqlException.fromSQL(stat.getSQL(), e); } finally { JdbcUtils.closeSilently(rs, true); } return result; } private SQLStatement getSelectStatement(boolean distinct) { SQLStatement stat = new SQLStatement(db); stat.appendSQL("SELECT "); if (distinct) { stat.appendSQL("DISTINCT "); } return stat; } /** * Begin a primitive boolean field condition clause. * * @param x * the primitive boolean field to query * @return a query condition to continue building the condition */ public QueryCondition where(boolean x) { from.getAliasDefinition().checkMultipleBooleans(); return wherePrimitive(x); } /** * Begin a primitive short field condition clause. * * @param x * the primitive short field to query * @return a query condition to continue building the condition */ public QueryCondition where(byte x) { return wherePrimitive(x); } /** * Begin a primitive short field condition clause. * * @param x * the primitive short field to query * @return a query condition to continue building the condition */ public QueryCondition where(short x) { return wherePrimitive(x); } /** * Begin a primitive int field condition clause. * * @param x * the primitive int field to query * @return a query condition to continue building the condition */ public QueryCondition where(int x) { return wherePrimitive(x); } /** * Begin a primitive long field condition clause. * * @param x * the primitive long field to query * @return a query condition to continue building the condition */ public QueryCondition where(long x) { return wherePrimitive(x); } /** * Begin a primitive float field condition clause. * * @param x * the primitive float field to query * @return a query condition to continue building the condition */ public QueryCondition where(float x) { return wherePrimitive(x); } /** * Begin a primitive double field condition clause. * * @param x * the primitive double field to query * @return a query condition to continue building the condition */ public QueryCondition where(double x) { return wherePrimitive(x); } /** * Begins a primitive field condition clause. * * @param value * @return a query condition to continue building the condition */ private QueryCondition wherePrimitive(A value) { A alias = getPrimitiveAliasByValue(value); if (alias == null) { // this will result in an unmapped field exception return where(value); } return where(alias); } /** * Begin an Object field condition clause. * * @param x * the mapped object to query * @return a query condition to continue building the condition */ public QueryCondition where(A x) { from.getAliasDefinition().checkMultipleEnums(x); return new QueryCondition(this, x); } public QueryWhere where(Filter filter) { HashMap fieldMap = Utils.newHashMap(); for (Field f : filter.getClass().getDeclaredFields()) { f.setAccessible(true); try { Object obj = f.get(filter); if (obj == from.getAlias()) { List fields = from.getAliasDefinition().getFields(); String name = f.getName(); for (TableDefinition.FieldDefinition field : fields) { String n = name + "." + field.field.getName(); Object o = field.field.get(obj); fieldMap.put(n, o); } } fieldMap.put(f.getName(), f.get(filter)); } catch (Exception e) { throw new IciqlException(e); } } Token filterCode = new ClassReader().decompile(filter, fieldMap, "where"); // String filterQuery = filterCode.toString(); conditions.add(filterCode); return new QueryWhere(this); } public QueryWhere where(String fragment, List args) { return this.where(fragment, args.toArray()); } public QueryWhere where(String fragment, Object... args) { conditions.add(new RuntimeToken(fragment, args)); return new QueryWhere(this); } public Query where(And conditions) { whereTrue(); addConditionToken(conditions.where.query); return this; } public Query where(Or conditions) { whereFalse(); addConditionToken(conditions.where.query); return this; } public QueryWhere whereTrue() { return whereTrue(true); } public QueryWhere whereFalse() { return whereTrue(false); } public QueryWhere whereTrue(Boolean condition) { Token token = new Function("", condition); addConditionToken(token); return new QueryWhere(this); } /** * Sets the Limit and Offset of a query. * * @return the query */ public Query limit(long limit) { this.limit = limit; return this; } public Query offset(long offset) { this.offset = offset; return this; } public Query orderBy(boolean field) { from.getAliasDefinition().checkMultipleBooleans(); return orderByPrimitive(field); } public Query orderBy(byte field) { return orderByPrimitive(field); } public Query orderBy(short field) { return orderByPrimitive(field); } public Query orderBy(int field) { return orderByPrimitive(field); } public Query orderBy(long field) { return orderByPrimitive(field); } public Query orderBy(float field) { return orderByPrimitive(field); } public Query orderBy(double field) { return orderByPrimitive(field); } Query orderByPrimitive(Object field) { Object alias = getPrimitiveAliasByValue(field); if (alias == null) { return orderBy(field); } return orderBy(alias); } public Query orderBy(Object expr) { from.getAliasDefinition().checkMultipleEnums(expr); OrderExpression e = new OrderExpression(this, expr, false, false, false); addOrderBy(e); return this; } /** * Order by a number of columns. * * @param expressions * the columns * @return the query */ public Query orderBy(Object... expressions) { for (Object expr : expressions) { from.getAliasDefinition().checkMultipleEnums(expr); OrderExpression e = new OrderExpression(this, expr, false, false, false); addOrderBy(e); } return this; } public Query orderByDesc(Object expr) { OrderExpression e = new OrderExpression(this, expr, true, false, false); addOrderBy(e); return this; } public Query groupBy(boolean field) { from.getAliasDefinition().checkMultipleBooleans(); return groupByPrimitive(field); } public Query groupBy(byte field) { return groupByPrimitive(field); } public Query groupBy(short field) { return groupByPrimitive(field); } public Query groupBy(int field) { return groupByPrimitive(field); } public Query groupBy(long field) { return groupByPrimitive(field); } public Query groupBy(float field) { return groupByPrimitive(field); } public Query groupBy(double field) { return groupByPrimitive(field); } Query groupByPrimitive(Object field) { Object alias = getPrimitiveAliasByValue(field); if (alias == null) { return groupBy(field); } return groupBy(alias); } public Query groupBy(Object expr) { from.getAliasDefinition().checkMultipleEnums(expr); groupByExpressions.add(expr); return this; } public Query groupBy(Object... groupBy) { this.groupByExpressions.addAll(Arrays.asList(groupBy)); return this; } /** * INTERNAL * * @param stat * the statement * @param alias * the alias object (can be null) * @param value * the value */ public void appendSQL(SQLStatement stat, Object alias, Object value) { if (Function.count() == value) { stat.appendSQL("COUNT(*)"); return; } if (RuntimeParameter.PARAMETER == value) { stat.appendSQL("?"); addParameter(stat, alias, value); return; } Token token = Db.getToken(value); if (token != null) { token.appendSQL(stat, this); return; } if (alias != null && value.getClass().isEnum()) { // special case: // value is first enum constant which is also the alias object. // the first enum constant is used as the alias because we can not // instantiate an enum reflectively. stat.appendSQL("?"); addParameter(stat, alias, value); return; } SelectColumn col = getColumnByReference(value); if (col != null) { col.appendSQL(stat); return; } stat.appendSQL("?"); addParameter(stat, alias, value); } /** * INTERNAL * * @param stat * the statement * @param alias * the alias object (can be null) * @param valueLeft * the value on the left of the compound clause * @param valueRight * the value on the right of the compound clause * @param compareType * the current compare type (e.g. BETWEEN) */ public void appendSQL(SQLStatement stat, Object alias, Object valueLeft, Object valueRight, CompareType compareType) { stat.appendSQL("?"); stat.appendSQL(" "); switch (compareType) { case BETWEEN: stat.appendSQL("AND"); break; } stat.appendSQL(" "); stat.appendSQL("?"); addParameter(stat, alias, valueLeft); addParameter(stat, alias, valueRight); } public void appendSQL(SQLStatement stat, Object alias, Iterable values, CompareType compareType) { boolean first = true; stat.appendSQL("("); for (Object value : values) { if (first) { first = false; } else { stat.appendSQL(", "); } stat.appendSQL("?"); addParameter(stat, alias, value); } stat.appendSQL(")"); } private void addParameter(SQLStatement stat, Object alias, Object value) { SelectColumn col = getColumnByReference(alias); if (col != null && value.getClass().isEnum()) { // enum EnumType type = col.getFieldDefinition().enumType; Enum anEnum = (Enum) value; Object y = Utils.convertEnum(anEnum, type); stat.addParameter(y); } else if (col != null) { // object Object parameter = db.getDialect().serialize(value, col.getFieldDefinition().typeAdapter); stat.addParameter(parameter); } else { // primitive stat.addParameter(value); } } void addConditionToken(Token condition) { if (condition == ConditionOpenClose.OPEN) { conditionDepth ++; } else if (condition == ConditionOpenClose.CLOSE) { conditionDepth --; if (conditionDepth < 0) { throw new IciqlException("unmatch condition open-close count"); } } conditions.add(condition); } void addConditionToken(Query other) { for (Token condition : other.conditions) { addConditionToken(condition); } } void addUpdateColumnDeclaration(UpdateColumn declaration) { updateColumnDeclarations.add(declaration); } void appendWhere(SQLStatement stat) { if (conditionDepth != 0) { throw new IciqlException("unmatch condition open-close count"); } if (!conditions.isEmpty()) { stat.appendSQL(" WHERE "); boolean skipNextConjunction = false; for (Token token : conditions) { if (skipNextConjunction && token instanceof ConditionAndOr) { skipNextConjunction = false; continue; } token.appendSQL(stat, this); stat.appendSQL(" "); if (ConditionOpenClose.OPEN == token) { skipNextConjunction = true; } } } } void appendFromWhere(SQLStatement stat) { appendFromWhere(stat, true); } void appendFromWhere(SQLStatement stat, boolean log) { stat.appendSQL(" FROM "); from.appendSQL(stat); for (SelectTable join : joins) { join.appendSQLAsJoin(stat, this); } appendWhere(stat); if (!groupByExpressions.isEmpty()) { stat.appendSQL(" GROUP BY "); int i = 0; for (Object obj : groupByExpressions) { if (i++ > 0) { stat.appendSQL(", "); } appendSQL(stat, null, obj); stat.appendSQL(" "); } } if (!orderByList.isEmpty()) { stat.appendSQL(" ORDER BY "); int i = 0; for (OrderExpression o : orderByList) { if (i++ > 0) { stat.appendSQL(", "); } o.appendSQL(stat); stat.appendSQL(" "); } } db.getDialect().appendLimitOffset(stat, limit, offset); if (log) { IciqlLogger.select(stat.getSQL()); } } /** * Join another table. * * @param alias * an alias for the table to join * @return the joined query */ public QueryJoin innerJoin(A alias) { return join(alias, false); } public QueryJoin leftJoin(A alias) { return join(alias, true); } @SuppressWarnings({ "unchecked", "rawtypes" }) private QueryJoin join(A alias, boolean outerJoin) { TableDefinition def = (TableDefinition) db.define(alias.getClass()); SelectTable join = new SelectTable(db, this, alias, outerJoin); def.initSelectObject(join, alias, aliasMap, false); joins.add(join); return new QueryJoin(this, join); } Db getDb() { return db; } SelectTable getFrom() { return from; } boolean isJoin() { return !joins.isEmpty(); } SelectTable getSelectTable(Object alias) { if (from.getAlias() == alias) { return from; } else { for (SelectTable join : joins) { if (join.getAlias() == alias) { return join; } } } return null; } /** * This method returns a mapped Object field by its reference. * * @param obj * @return */ private SelectColumn getColumnByReference(Object obj) { SelectColumn col = aliasMap.get(obj); return col; } /** * This method returns the alias of a mapped primitive field by its value. * * @param obj * @return */ @SuppressWarnings("unchecked") A getPrimitiveAliasByValue(A obj) { for (Object alias : aliasMap.keySet()) { if (alias.equals(obj)) { SelectColumn match = aliasMap.get(alias); if (match.getFieldDefinition().isPrimitive) { return (A) alias; } } } return null; } void addOrderBy(OrderExpression expr) { orderByList.add(expr); } }