From d8915c7da130b8a6de6f2c911effe0e10dbe4d12 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 8 Mar 2013 21:14:50 -0500 Subject: Conform to Apache standard directory layout --- src/main/java/com/iciql/CompareType.java | 45 + src/main/java/com/iciql/Condition.java | 55 + src/main/java/com/iciql/ConditionAndOr.java | 37 + src/main/java/com/iciql/Constants.java | 38 + src/main/java/com/iciql/Db.java | 774 +++++++++++ src/main/java/com/iciql/DbInspector.java | 204 +++ src/main/java/com/iciql/DbUpgrader.java | 81 ++ src/main/java/com/iciql/DbVersion.java | 55 + src/main/java/com/iciql/Define.java | 145 ++ src/main/java/com/iciql/Filter.java | 25 + src/main/java/com/iciql/Function.java | 149 ++ src/main/java/com/iciql/Iciql.java | 731 ++++++++++ src/main/java/com/iciql/IciqlException.java | 177 +++ src/main/java/com/iciql/ModelUtils.java | 499 +++++++ src/main/java/com/iciql/OrderExpression.java | 55 + src/main/java/com/iciql/Query.java | 947 +++++++++++++ src/main/java/com/iciql/QueryBetween.java | 60 + src/main/java/com/iciql/QueryCondition.java | 128 ++ src/main/java/com/iciql/QueryJoin.java | 75 + src/main/java/com/iciql/QueryJoinCondition.java | 83 ++ src/main/java/com/iciql/QueryWhere.java | 501 +++++++ src/main/java/com/iciql/RuntimeParameter.java | 49 + src/main/java/com/iciql/RuntimeToken.java | 57 + src/main/java/com/iciql/SQLDialect.java | 206 +++ src/main/java/com/iciql/SQLDialectDefault.java | 445 ++++++ src/main/java/com/iciql/SQLDialectDerby.java | 71 + src/main/java/com/iciql/SQLDialectH2.java | 135 ++ src/main/java/com/iciql/SQLDialectHSQL.java | 149 ++ src/main/java/com/iciql/SQLDialectMSSQL.java | 53 + src/main/java/com/iciql/SQLDialectMySQL.java | 93 ++ src/main/java/com/iciql/SQLDialectPostgreSQL.java | 103 ++ src/main/java/com/iciql/SQLStatement.java | 190 +++ src/main/java/com/iciql/SelectColumn.java | 57 + src/main/java/com/iciql/SelectTable.java | 112 ++ src/main/java/com/iciql/SubQuery.java | 32 + src/main/java/com/iciql/SubQueryCondition.java | 41 + src/main/java/com/iciql/TableDefinition.java | 1233 +++++++++++++++++ src/main/java/com/iciql/TableInspector.java | 723 ++++++++++ src/main/java/com/iciql/TestCondition.java | 115 ++ src/main/java/com/iciql/Token.java | 35 + src/main/java/com/iciql/UpdateColumn.java | 35 + src/main/java/com/iciql/UpdateColumnIncrement.java | 55 + src/main/java/com/iciql/UpdateColumnSet.java | 63 + src/main/java/com/iciql/ValidationRemark.java | 127 ++ src/main/java/com/iciql/bytecode/And.java | 46 + src/main/java/com/iciql/bytecode/ArrayGet.java | 49 + src/main/java/com/iciql/bytecode/CaseWhen.java | 62 + src/main/java/com/iciql/bytecode/ClassReader.java | 1457 ++++++++++++++++++++ src/main/java/com/iciql/bytecode/Constant.java | 38 + .../java/com/iciql/bytecode/ConstantNumber.java | 70 + .../java/com/iciql/bytecode/ConstantString.java | 55 + src/main/java/com/iciql/bytecode/Function.java | 47 + src/main/java/com/iciql/bytecode/Not.java | 55 + src/main/java/com/iciql/bytecode/Null.java | 44 + src/main/java/com/iciql/bytecode/Operation.java | 111 ++ src/main/java/com/iciql/bytecode/Or.java | 47 + src/main/java/com/iciql/bytecode/Variable.java | 51 + src/main/java/com/iciql/bytecode/package.html | 25 + src/main/java/com/iciql/package.html | 25 + src/main/java/com/iciql/util/GenerateModels.java | 193 +++ src/main/java/com/iciql/util/IciqlLogger.java | 214 +++ src/main/java/com/iciql/util/JdbcUtils.java | 254 ++++ .../java/com/iciql/util/Slf4jIciqlListener.java | 92 ++ src/main/java/com/iciql/util/StatementBuilder.java | 166 +++ src/main/java/com/iciql/util/StringUtils.java | 382 +++++ src/main/java/com/iciql/util/Utils.java | 459 ++++++ .../java/com/iciql/util/WeakIdentityHashMap.java | 243 ++++ src/main/java/com/iciql/util/package.html | 25 + 68 files changed, 13253 insertions(+) create mode 100644 src/main/java/com/iciql/CompareType.java create mode 100644 src/main/java/com/iciql/Condition.java create mode 100644 src/main/java/com/iciql/ConditionAndOr.java create mode 100644 src/main/java/com/iciql/Constants.java create mode 100644 src/main/java/com/iciql/Db.java create mode 100644 src/main/java/com/iciql/DbInspector.java create mode 100644 src/main/java/com/iciql/DbUpgrader.java create mode 100644 src/main/java/com/iciql/DbVersion.java create mode 100644 src/main/java/com/iciql/Define.java create mode 100644 src/main/java/com/iciql/Filter.java create mode 100644 src/main/java/com/iciql/Function.java create mode 100644 src/main/java/com/iciql/Iciql.java create mode 100644 src/main/java/com/iciql/IciqlException.java create mode 100644 src/main/java/com/iciql/ModelUtils.java create mode 100644 src/main/java/com/iciql/OrderExpression.java create mode 100644 src/main/java/com/iciql/Query.java create mode 100644 src/main/java/com/iciql/QueryBetween.java create mode 100644 src/main/java/com/iciql/QueryCondition.java create mode 100644 src/main/java/com/iciql/QueryJoin.java create mode 100644 src/main/java/com/iciql/QueryJoinCondition.java create mode 100644 src/main/java/com/iciql/QueryWhere.java create mode 100644 src/main/java/com/iciql/RuntimeParameter.java create mode 100644 src/main/java/com/iciql/RuntimeToken.java create mode 100644 src/main/java/com/iciql/SQLDialect.java create mode 100644 src/main/java/com/iciql/SQLDialectDefault.java create mode 100644 src/main/java/com/iciql/SQLDialectDerby.java create mode 100644 src/main/java/com/iciql/SQLDialectH2.java create mode 100644 src/main/java/com/iciql/SQLDialectHSQL.java create mode 100644 src/main/java/com/iciql/SQLDialectMSSQL.java create mode 100644 src/main/java/com/iciql/SQLDialectMySQL.java create mode 100644 src/main/java/com/iciql/SQLDialectPostgreSQL.java create mode 100644 src/main/java/com/iciql/SQLStatement.java create mode 100644 src/main/java/com/iciql/SelectColumn.java create mode 100644 src/main/java/com/iciql/SelectTable.java create mode 100644 src/main/java/com/iciql/SubQuery.java create mode 100644 src/main/java/com/iciql/SubQueryCondition.java create mode 100644 src/main/java/com/iciql/TableDefinition.java create mode 100644 src/main/java/com/iciql/TableInspector.java create mode 100644 src/main/java/com/iciql/TestCondition.java create mode 100644 src/main/java/com/iciql/Token.java create mode 100644 src/main/java/com/iciql/UpdateColumn.java create mode 100644 src/main/java/com/iciql/UpdateColumnIncrement.java create mode 100644 src/main/java/com/iciql/UpdateColumnSet.java create mode 100644 src/main/java/com/iciql/ValidationRemark.java create mode 100644 src/main/java/com/iciql/bytecode/And.java create mode 100644 src/main/java/com/iciql/bytecode/ArrayGet.java create mode 100644 src/main/java/com/iciql/bytecode/CaseWhen.java create mode 100644 src/main/java/com/iciql/bytecode/ClassReader.java create mode 100644 src/main/java/com/iciql/bytecode/Constant.java create mode 100644 src/main/java/com/iciql/bytecode/ConstantNumber.java create mode 100644 src/main/java/com/iciql/bytecode/ConstantString.java create mode 100644 src/main/java/com/iciql/bytecode/Function.java create mode 100644 src/main/java/com/iciql/bytecode/Not.java create mode 100644 src/main/java/com/iciql/bytecode/Null.java create mode 100644 src/main/java/com/iciql/bytecode/Operation.java create mode 100644 src/main/java/com/iciql/bytecode/Or.java create mode 100644 src/main/java/com/iciql/bytecode/Variable.java create mode 100644 src/main/java/com/iciql/bytecode/package.html create mode 100644 src/main/java/com/iciql/package.html create mode 100644 src/main/java/com/iciql/util/GenerateModels.java create mode 100644 src/main/java/com/iciql/util/IciqlLogger.java create mode 100644 src/main/java/com/iciql/util/JdbcUtils.java create mode 100644 src/main/java/com/iciql/util/Slf4jIciqlListener.java create mode 100644 src/main/java/com/iciql/util/StatementBuilder.java create mode 100644 src/main/java/com/iciql/util/StringUtils.java create mode 100644 src/main/java/com/iciql/util/Utils.java create mode 100644 src/main/java/com/iciql/util/WeakIdentityHashMap.java create mode 100644 src/main/java/com/iciql/util/package.html (limited to 'src/main/java') diff --git a/src/main/java/com/iciql/CompareType.java b/src/main/java/com/iciql/CompareType.java new file mode 100644 index 0000000..84e29fe --- /dev/null +++ b/src/main/java/com/iciql/CompareType.java @@ -0,0 +1,45 @@ +/* + * 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; + +/** + * An enumeration of compare operations. + */ + +enum CompareType { + EQUAL("=", true), EXCEEDS(">", true), AT_LEAST(">=", true), LESS_THAN("<", true), AT_MOST("<=", true), NOT_EQUAL( + "<>", true), IS_NOT_NULL("IS NOT NULL", false), IS_NULL("IS NULL", false), LIKE("LIKE", true), BETWEEN( + "BETWEEN", true); + + private String text; + private boolean hasRightExpression; + + CompareType(String text, boolean hasRightExpression) { + this.text = text; + this.hasRightExpression = hasRightExpression; + } + + String getString() { + return text; + } + + boolean hasRightExpression() { + return hasRightExpression; + } + +} diff --git a/src/main/java/com/iciql/Condition.java b/src/main/java/com/iciql/Condition.java new file mode 100644 index 0000000..17cb117 --- /dev/null +++ b/src/main/java/com/iciql/Condition.java @@ -0,0 +1,55 @@ +/* + * 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; + +/** + * A condition contains one or two operands and a compare operation. + * + * @param + * the operand type + */ + +class Condition implements Token { + CompareType compareType; + A x, y, z; + + Condition(A x, A y, CompareType compareType) { + this(x, y, null, compareType); + } + + Condition(A x, A y, A z, CompareType compareType) { + this.compareType = compareType; + this.x = x; + this.y = y; + this.z = z; + } + + public void appendSQL(SQLStatement stat, Query query) { + query.appendSQL(stat, null, x); + stat.appendSQL(" "); + stat.appendSQL(compareType.getString()); + if (compareType.hasRightExpression()) { + stat.appendSQL(" "); + if (z == null) { + query.appendSQL(stat, x, y); + } else { + query.appendSQL(stat, x, y, z, compareType); + } + } + } +} diff --git a/src/main/java/com/iciql/ConditionAndOr.java b/src/main/java/com/iciql/ConditionAndOr.java new file mode 100644 index 0000000..4d1cd0e --- /dev/null +++ b/src/main/java/com/iciql/ConditionAndOr.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * An OR or an AND condition. + */ + +enum ConditionAndOr implements Token { + AND("AND"), OR("OR"); + + private String text; + + ConditionAndOr(String text) { + this.text = text; + } + + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL(text); + } + +} diff --git a/src/main/java/com/iciql/Constants.java b/src/main/java/com/iciql/Constants.java new file mode 100644 index 0000000..8b54493 --- /dev/null +++ b/src/main/java/com/iciql/Constants.java @@ -0,0 +1,38 @@ +/* + * 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; + +/** + * Iciql constants. + */ +public class Constants { + + public static final String NAME = "iciql"; + + // The build script extracts this exact line so be careful editing it + // and only use A-Z a-z 0-9 .-_ in the string. + public static final String VERSION = "1.2.0-SNAPSHOT"; + + // The build script extracts this exact line so be careful editing it + // and only use A-Z a-z 0-9 .-_ in the string. + public static final String VERSION_DATE = "PENDING"; + + // The build script extracts this exact line so be careful editing it + // and only use A-Z a-z 0-9 .-_ in the string. + public static final String API_CURRENT = "15"; + +} diff --git a/src/main/java/com/iciql/Db.java b/src/main/java/com/iciql/Db.java new file mode 100644 index 0000000..ecd373c --- /dev/null +++ b/src/main/java/com/iciql/Db.java @@ -0,0 +1,774 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * Copyright 2012 Frédéric Gaillard. + * Copyright 2012 Alex Telepov. + * + * 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.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Savepoint; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.sql.DataSource; + +import com.iciql.DbUpgrader.DefaultDbUpgrader; +import com.iciql.Iciql.IQTable; +import com.iciql.Iciql.IQVersion; +import com.iciql.Iciql.IQView; +import com.iciql.util.IciqlLogger; +import com.iciql.util.JdbcUtils; +import com.iciql.util.StringUtils; +import com.iciql.util.Utils; +import com.iciql.util.WeakIdentityHashMap; + +/** + * This class represents a connection to a database. + */ + +public class Db { + + /** + * This map It holds unique tokens that are generated by functions such as + * Function.sum(..) in "db.from(p).select(Function.sum(p.unitPrice))". It + * doesn't actually hold column tokens, as those are bound to the query + * itself. + */ + private static final Map TOKENS; + + private static final Map> DIALECTS; + + private final Connection conn; + private final Map, TableDefinition> classMap = Collections + .synchronizedMap(new HashMap, TableDefinition>()); + private final SQLDialect dialect; + private DbUpgrader dbUpgrader = new DefaultDbUpgrader(); + private final Set> upgradeChecked = Collections.synchronizedSet(new HashSet>()); + + private boolean skipCreate; + private boolean autoSavePoint = true; + + static { + TOKENS = Collections.synchronizedMap(new WeakIdentityHashMap()); + DIALECTS = Collections.synchronizedMap(new HashMap>()); + // can register by... + // 1. Connection class name + // 2. DatabaseMetaData.getDatabaseProductName() + DIALECTS.put("Apache Derby", SQLDialectDerby.class); + DIALECTS.put("H2", SQLDialectH2.class); + DIALECTS.put("HSQL Database Engine", SQLDialectHSQL.class); + DIALECTS.put("MySQL", SQLDialectMySQL.class); + DIALECTS.put("PostgreSQL", SQLDialectPostgreSQL.class); + DIALECTS.put("Microsoft SQL Server", SQLDialectMSSQL.class); + } + + private Db(Connection conn) { + this.conn = conn; + String databaseName = null; + DatabaseMetaData data = null; + try { + data = conn.getMetaData(); + databaseName = data.getDatabaseProductName(); + } catch (SQLException s) { + throw new IciqlException(s, "failed to retrieve database metadata!"); + } + dialect = getDialect(databaseName, conn.getClass().getName()); + dialect.configureDialect(databaseName, data); + } + + /** + * Register a new/custom dialect class. You can use this method to replace + * any existing dialect or to add a new one. + * + * @param token + * the fully qualified name of the connection class or the + * expected result of DatabaseMetaData.getDatabaseProductName() + * @param dialectClass + * the dialect class to register + */ + public static void registerDialect(String token, Class dialectClass) { + DIALECTS.put(token, dialectClass); + } + + SQLDialect getDialect(String databaseName, String className) { + Class dialectClass = null; + if (DIALECTS.containsKey(className)) { + // dialect registered by connection class name + dialectClass = DIALECTS.get(className); + } else if (DIALECTS.containsKey(databaseName)) { + // dialect registered by database name + dialectClass = DIALECTS.get(databaseName); + } else { + // did not find a match, use default + dialectClass = SQLDialectDefault.class; + } + return instance(dialectClass); + } + + static X registerToken(X x, Token token) { + TOKENS.put(x, token); + return x; + } + + static Token getToken(Object x) { + return TOKENS.get(x); + } + + static T instance(Class clazz) { + try { + return clazz.newInstance(); + } catch (Exception e) { + throw new IciqlException(e); + } + } + + public static Db open(String url) { + try { + Connection conn = JdbcUtils.getConnection(null, url, null, null); + return new Db(conn); + } catch (SQLException e) { + throw new IciqlException(e); + } + } + + public static Db open(String url, String user, String password) { + try { + Connection conn = JdbcUtils.getConnection(null, url, user, password); + return new Db(conn); + } catch (SQLException e) { + throw new IciqlException(e); + } + } + + public static Db open(String url, String user, char[] password) { + try { + Connection conn = JdbcUtils.getConnection(null, url, user, password == null ? null : new String(password)); + return new Db(conn); + } catch (SQLException e) { + throw new IciqlException(e); + } + } + + /** + * Create a new database instance using a data source. This method is fast, + * so that you can always call open() / close() on usage. + * + * @param ds + * the data source + * @return the database instance. + */ + public static Db open(DataSource ds) { + try { + return new Db(ds.getConnection()); + } catch (SQLException e) { + throw new IciqlException(e); + } + } + + public static Db open(Connection conn) { + return new Db(conn); + } + + + + /** + * Convenience function to avoid import statements in application code. + */ + public void activateConsoleLogger() { + IciqlLogger.activateConsoleLogger(); + } + + /** + * Convenience function to avoid import statements in application code. + */ + public void deactivateConsoleLogger() { + IciqlLogger.deactivateConsoleLogger(); + } + + public void insert(T t) { + Class clazz = t.getClass(); + long rc = define(clazz).createIfRequired(this).insert(this, t, false); + if (rc == 0) { + throw new IciqlException("Failed to insert {0}. Affected rowcount == 0.", t); + } + } + + public long insertAndGetKey(T t) { + Class clazz = t.getClass(); + return define(clazz).createIfRequired(this).insert(this, t, true); + } + + /** + * Merge INSERTS if the record does not exist or UPDATES the record if it + * does exist. Not all databases support MERGE and the syntax varies with + * the database. + * + * If the database does not support a MERGE syntax the dialect can try to + * simulate a merge by implementing: + *

+ * INSERT INTO foo... (SELECT ?,... FROM foo WHERE pk=? HAVING count(*)=0) + *

+ * iciql will check the affected row count returned by the internal merge + * method and if the affected row count = 0, it will issue an update. + *

+ * See the Derby dialect for an implementation of this technique. + *

+ * If the dialect does not support merge an IciqlException will be thrown. + * + * @param t + */ + public void merge(T t) { + Class clazz = t.getClass(); + TableDefinition def = define(clazz).createIfRequired(this); + int rc = def.merge(this, t); + if (rc == 0) { + rc = def.update(this, t); + } + if (rc == 0) { + throw new IciqlException("merge failed"); + } + } + + public int update(T t) { + Class clazz = t.getClass(); + return define(clazz).createIfRequired(this).update(this, t); + } + + public int delete(T t) { + Class clazz = t.getClass(); + return define(clazz).createIfRequired(this).delete(this, t); + } + + public Query from(T alias) { + Class clazz = alias.getClass(); + define(clazz).createIfRequired(this); + return Query.from(this, alias); + } + + @SuppressWarnings("unchecked") + public int dropTable(Class modelClass) { + TableDefinition def = (TableDefinition) define(modelClass); + SQLStatement stat = new SQLStatement(this); + getDialect().prepareDropTable(stat, def); + IciqlLogger.drop(stat.getSQL()); + int rc = 0; + try { + rc = stat.executeUpdate(); + } catch (IciqlException e) { + if (e.getIciqlCode() != IciqlException.CODE_OBJECT_NOT_FOUND) { + throw e; + } + } + // remove this model class from the table definition cache + classMap.remove(modelClass); + // remove this model class from the upgrade checked cache + upgradeChecked.remove(modelClass); + return rc; + } + + @SuppressWarnings("unchecked") + public int dropView(Class modelClass) { + TableDefinition def = (TableDefinition) define(modelClass); + SQLStatement stat = new SQLStatement(this); + getDialect().prepareDropView(stat, def); + IciqlLogger.drop(stat.getSQL()); + int rc = 0; + try { + rc = stat.executeUpdate(); + } catch (IciqlException e) { + if (e.getIciqlCode() != IciqlException.CODE_OBJECT_NOT_FOUND) { + throw e; + } + } + // remove this model class from the table definition cache + classMap.remove(modelClass); + // remove this model class from the upgrade checked cache + upgradeChecked.remove(modelClass); + return rc; + } + + public List buildObjects(Class modelClass, ResultSet rs) { + return buildObjects(modelClass, false, rs); + } + + @SuppressWarnings("unchecked") + public List buildObjects(Class modelClass, boolean wildcardSelect, ResultSet rs) { + List result = new ArrayList(); + TableDefinition def = (TableDefinition) define(modelClass); + try { + int[] columns = def.mapColumns(wildcardSelect, rs); + while (rs.next()) { + T item = Utils.newObject(modelClass); + def.readRow(item, rs, columns); + result.add(item); + } + } catch (SQLException e) { + throw new IciqlException(e); + } + return result; + } + + Db upgradeDb() { + if (!upgradeChecked.contains(dbUpgrader.getClass())) { + // flag as checked immediately because calls are nested. + upgradeChecked.add(dbUpgrader.getClass()); + + IQVersion model = dbUpgrader.getClass().getAnnotation(IQVersion.class); + if (model.value() == 0) { + // try superclass + Class superClass = dbUpgrader.getClass().getSuperclass(); + if (superClass.isAnnotationPresent(IQVersion.class)) { + model = superClass.getAnnotation(IQVersion.class); + } + } + if (model.value() > 0) { + DbVersion v = new DbVersion(); + // (SCHEMA="" && TABLE="") == DATABASE + DbVersion dbVersion = from(v).where(v.schemaName).is("").and(v.tableName).is("") + .selectFirst(); + if (dbVersion == null) { + // database has no version registration, but model specifies + // version: insert DbVersion entry and return. + DbVersion newDb = new DbVersion(model.value()); + // database is an older version than the model + boolean success = dbUpgrader.upgradeDatabase(this, 0, newDb.version); + if (success) { + insert(newDb); + } + } else { + // database has a version registration: + // check to see if upgrade is required. + if ((model.value() > dbVersion.version) && (dbUpgrader != null)) { + // database is an older version than the model + boolean success = dbUpgrader.upgradeDatabase(this, dbVersion.version, model.value()); + if (success) { + dbVersion.version = model.value(); + update(dbVersion); + } + } + } + } + } + return this; + } + + void upgradeTable(TableDefinition model) { + if (!upgradeChecked.contains(model.getModelClass())) { + // flag is checked immediately because calls are nested + upgradeChecked.add(model.getModelClass()); + + if (model.tableVersion > 0) { + // table is using iciql version tracking. + DbVersion v = new DbVersion(); + String schema = StringUtils.isNullOrEmpty(model.schemaName) ? "" : model.schemaName; + DbVersion dbVersion = from(v).where(v.schemaName).is(schema).and(v.tableName) + .is(model.tableName).selectFirst(); + if (dbVersion == null) { + // table has no version registration, but model specifies + // version: insert DbVersion entry + DbVersion newTable = new DbVersion(model.tableVersion); + newTable.schemaName = schema; + newTable.tableName = model.tableName; + insert(newTable); + } else { + // table has a version registration: + // check if upgrade is required + if ((model.tableVersion > dbVersion.version) && (dbUpgrader != null)) { + // table is an older version than model + boolean success = dbUpgrader.upgradeTable(this, schema, model.tableName, + dbVersion.version, model.tableVersion); + if (success) { + dbVersion.version = model.tableVersion; + update(dbVersion); + } + } + } + } + } + } + + TableDefinition define(Class clazz) { + TableDefinition def = getTableDefinition(clazz); + if (def == null) { + upgradeDb(); + def = new TableDefinition(clazz); + def.mapFields(); + classMap.put(clazz, def); + if (Iciql.class.isAssignableFrom(clazz)) { + T t = instance(clazz); + Iciql table = (Iciql) t; + Define.define(def, table); + } else if (clazz.isAnnotationPresent(IQTable.class)) { + // annotated classes skip the Define().define() static + // initializer + T t = instance(clazz); + def.mapObject(t); + } else if (clazz.isAnnotationPresent(IQView.class)) { + // annotated classes skip the Define().define() static + // initializer + T t = instance(clazz); + def.mapObject(t); + } + } + return def; + } + + boolean hasCreated(Class clazz) { + return upgradeChecked.contains(clazz); + } + + public synchronized void setDbUpgrader(DbUpgrader upgrader) { + if (!upgrader.getClass().isAnnotationPresent(IQVersion.class)) { + throw new IciqlException("DbUpgrader must be annotated with " + IQVersion.class.getSimpleName()); + } + this.dbUpgrader = upgrader; + upgradeChecked.clear(); + } + + public SQLDialect getDialect() { + return dialect; + } + + public Connection getConnection() { + return conn; + } + + public void close() { + try { + conn.close(); + } catch (Exception e) { + throw new IciqlException(e); + } + } + + public TestCondition test(A x) { + return new TestCondition(x); + } + + public void insertAll(List list) { + if (list.size() == 0) { + return; + } + Savepoint savepoint = null; + try { + Class clazz = list.get(0).getClass(); + TableDefinition def = define(clazz).createIfRequired(this); + savepoint = prepareSavepoint(); + for (T t : list) { + PreparedStatement ps = def.createInsertStatement(this, t, false); + int rc = ps.executeUpdate(); + if (rc == 0) { + throw new IciqlException("Failed to insert {0}. Affected rowcount == 0.", t); + } + } + commit(savepoint); + } catch (SQLException e) { + rollback(savepoint); + throw new IciqlException(e); + } catch (IciqlException e) { + rollback(savepoint); + throw e; + } + } + + public List insertAllAndGetKeys(List list) { + List identities = new ArrayList(); + if (list.size() == 0) { + return identities; + } + Savepoint savepoint = null; + try { + Class clazz = list.get(0).getClass(); + TableDefinition def = define(clazz).createIfRequired(this); + savepoint = prepareSavepoint(); + for (T t : list) { + long key = def.insert(this, t, true); + identities.add(key); + } + commit(savepoint); + } catch (IciqlException e) { + rollback(savepoint); + throw e; + } + return identities; + } + + public void updateAll(List list) { + if (list.size() == 0) { + return; + } + Savepoint savepoint = null; + try { + Class clazz = list.get(0).getClass(); + TableDefinition def = define(clazz).createIfRequired(this); + savepoint = prepareSavepoint(); + for (T t : list) { + def.update(this, t); + } + commit(savepoint); + } catch (IciqlException e) { + rollback(savepoint); + throw e; + } + } + + public void deleteAll(List list) { + if (list.size() == 0) { + return; + } + Savepoint savepoint = null; + try { + Class clazz = list.get(0).getClass(); + TableDefinition def = define(clazz).createIfRequired(this); + savepoint = prepareSavepoint(); + for (T t : list) { + def.delete(this, t); + } + commit(savepoint); + } catch (IciqlException e) { + rollback(savepoint); + throw e; + } + } + + PreparedStatement prepare(String sql, boolean returnGeneratedKeys) { + IciqlException.checkUnmappedField(sql); + try { + if (returnGeneratedKeys) { + return conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + } + return conn.prepareStatement(sql); + } catch (SQLException e) { + throw IciqlException.fromSQL(sql, e); + } + } + + Savepoint prepareSavepoint() { + // don't change auto-commit mode. + // don't create save point. + if (!autoSavePoint) { + return null; + } + // create a savepoint + Savepoint savepoint = null; + try { + conn.setAutoCommit(false); + savepoint = conn.setSavepoint(); + } catch (SQLFeatureNotSupportedException e) { + // jdbc driver does not support save points + } catch (SQLException e) { + throw new IciqlException(e, "Could not create save point"); + } + return savepoint; + } + + void commit(Savepoint savepoint) { + if (savepoint != null) { + try { + conn.commit(); + conn.setAutoCommit(true); + } catch (SQLException e) { + throw new IciqlException(e, "Failed to commit pending transactions"); + } + } + } + + void rollback(Savepoint savepoint) { + if (savepoint != null) { + try { + conn.rollback(savepoint); + conn.setAutoCommit(true); + } catch (SQLException s) { + throw new IciqlException(s, "Failed to rollback transactions"); + } + } + } + + @SuppressWarnings("unchecked") + TableDefinition getTableDefinition(Class clazz) { + return (TableDefinition) classMap.get(clazz); + } + + /** + * Run a SQL query directly against the database. + * + * Be sure to close the ResultSet with + * + *

+	 * JdbcUtils.closeSilently(rs, true);
+	 * 
+ * + * @param sql + * the SQL statement + * @param args + * optional object arguments for x=? tokens in query + * @return the result set + */ + public ResultSet executeQuery(String sql, List args) { + return executeQuery(sql, args.toArray()); + } + + /** + * Run a SQL query directly against the database. + * + * Be sure to close the ResultSet with + * + *
+	 * JdbcUtils.closeSilently(rs, true);
+	 * 
+ * + * @param sql + * the SQL statement + * @param args + * optional object arguments for x=? tokens in query + * @return the result set + */ + public ResultSet executeQuery(String sql, Object... args) { + try { + if (args.length == 0) { + return conn.createStatement().executeQuery(sql); + } else { + PreparedStatement stat = conn.prepareStatement(sql); + int i = 1; + for (Object arg : args) { + stat.setObject(i++, arg); + } + return stat.executeQuery(); + } + } catch (SQLException e) { + throw new IciqlException(e); + } + } + + /** + * Run a SQL query directly against the database and map the results to the + * model class. + * + * @param modelClass + * the model class to bind the query ResultSet rows into. + * @param sql + * the SQL statement + * @return the result set + */ + public List executeQuery(Class modelClass, String sql, List args) { + return executeQuery(modelClass, sql, args.toArray()); + } + + /** + * Run a SQL query directly against the database and map the results to the + * model class. + * + * @param modelClass + * the model class to bind the query ResultSet rows into. + * @param sql + * the SQL statement + * @return the result set + */ + public List executeQuery(Class modelClass, String sql, Object... args) { + ResultSet rs = null; + try { + if (args.length == 0) { + rs = conn.createStatement().executeQuery(sql); + } else { + PreparedStatement stat = conn.prepareStatement(sql); + int i = 1; + for (Object arg : args) { + stat.setObject(i++, arg); + } + rs = stat.executeQuery(); + } + boolean wildcardSelect = sql.toLowerCase().startsWith("select *") + || sql.toLowerCase().startsWith("select distinct *"); + return buildObjects(modelClass, wildcardSelect, rs); + } catch (SQLException e) { + throw new IciqlException(e); + } finally { + JdbcUtils.closeSilently(rs, true); + } + } + + /** + * Run a SQL statement directly against the database. + * + * @param sql + * the SQL statement + * @return the update count + */ + public int executeUpdate(String sql, Object... args) { + Statement stat = null; + try { + int updateCount; + if (args.length == 0) { + stat = conn.createStatement(); + updateCount = stat.executeUpdate(sql); + } else { + PreparedStatement ps = conn.prepareStatement(sql); + int i = 1; + for (Object arg : args) { + ps.setObject(i++, arg); + } + updateCount = ps.executeUpdate(); + stat = ps; + } + return updateCount; + } catch (SQLException e) { + throw new IciqlException(e); + } finally { + JdbcUtils.closeSilently(stat); + } + } + + /** + * Allow to enable/disable globally createIfRequired in TableDefinition. + * For advanced user wanting to gain full control of transactions. + * Default value is false. + * @param skipCreate + */ + public void setSkipCreate(boolean skipCreate) { + this.skipCreate = skipCreate; + } + + public boolean getSkipCreate() { + return this.skipCreate; + } + + /** + * Allow to enable/disable usage of save point. + * For advanced user wanting to gain full control of transactions. + * Default value is false. + * @param autoSavePoint + */ + public void setAutoSavePoint(boolean autoSavePoint) { + this.autoSavePoint = autoSavePoint; + } + + public boolean getAutoSavePoint() { + return this.autoSavePoint; + } + +} diff --git a/src/main/java/com/iciql/DbInspector.java b/src/main/java/com/iciql/DbInspector.java new file mode 100644 index 0000000..acaceea --- /dev/null +++ b/src/main/java/com/iciql/DbInspector.java @@ -0,0 +1,204 @@ +/* + * 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.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; + +import com.iciql.Iciql.IQTable; +import com.iciql.util.JdbcUtils; +import com.iciql.util.StringUtils; +import com.iciql.util.Utils; + +/** + * Class to inspect a model and a database for the purposes of model validation + * and automatic model generation. This class finds the available schemas and + * tables and serves as the entry point for model generation and validation. + */ +public class DbInspector { + + private Db db; + private DatabaseMetaData metaData; + private Class dateTimeClass = java.util.Date.class; + + public DbInspector(Db db) { + this.db = db; + setPreferredDateTimeClass(db.getDialect().getDateTimeClass()); + } + + /** + * Set the preferred class to store date and time. Possible values are: + * java.util.Date (default) and java.sql.Timestamp. + * + * @param dateTimeClass + * the new class + */ + public void setPreferredDateTimeClass(Class dateTimeClass) { + this.dateTimeClass = dateTimeClass; + } + + /** + * Generates models class skeletons for schemas and tables. If the table + * name is undefined, models will be generated for every table within the + * specified schema. Additionally, if no schema is defined, models will be + * generated for all schemas and all tables. + * + * @param schema + * the schema name (optional) + * @param table + * the table name (optional) + * @param packageName + * the package name (optional) + * @param annotateSchema + * (includes schema name in annotation) + * @param trimStrings + * (trims strings to maxLength of column) + * @return a list of complete model classes as strings, each element a class + */ + public List generateModel(String schema, String table, String packageName, + boolean annotateSchema, boolean trimStrings) { + try { + List models = Utils.newArrayList(); + List tables = getTables(schema, table); + for (TableInspector t : tables) { + t.read(metaData); + String model = t.generateModel(packageName, annotateSchema, trimStrings); + models.add(model); + } + return models; + } catch (SQLException s) { + throw new IciqlException(s); + } + } + + /** + * Validates a model. + * + * @param model + * an instance of the model class + * @param throwOnError + * if errors should cause validation to fail + * @return a list of validation remarks + */ + public List validateModel(T model, boolean throwOnError) { + try { + TableInspector inspector = getTable(model); + inspector.read(metaData); + @SuppressWarnings("unchecked") + Class clazz = (Class) model.getClass(); + TableDefinition def = db.define(clazz); + return inspector.validate(def, throwOnError); + } catch (SQLException s) { + throw new IciqlException(s); + } + } + + private DatabaseMetaData getMetaData() throws SQLException { + if (metaData == null) { + metaData = db.getConnection().getMetaData(); + } + return metaData; + } + + /** + * Get the table in the database based on the model definition. + * + * @param model + * an instance of the model class + * @return the table inspector + */ + private TableInspector getTable(T model) throws SQLException { + @SuppressWarnings("unchecked") + Class clazz = (Class) model.getClass(); + TableDefinition def = db.define(clazz); + boolean forceUpperCase = getMetaData().storesUpperCaseIdentifiers(); + String schema = (forceUpperCase && def.schemaName != null) ? def.schemaName.toUpperCase() + : def.schemaName; + String table = forceUpperCase ? def.tableName.toUpperCase() : def.tableName; + List tables = getTables(schema, table); + return tables.get(0); + } + + /** + * Returns a list of tables. This method always returns at least one + * element. If no table is found, an exception is thrown. + * + * @param schema + * the schema name + * @param table + * the table name + * @return a list of table inspectors (always contains at least one element) + */ + private List getTables(String schema, String table) throws SQLException { + ResultSet rs = null; + try { + rs = getMetaData().getSchemas(); + ArrayList schemaList = Utils.newArrayList(); + while (rs.next()) { + schemaList.add(rs.getString("TABLE_SCHEM")); + } + JdbcUtils.closeSilently(rs); + + String iciqlTables = DbVersion.class.getAnnotation(IQTable.class).name(); + + List tables = Utils.newArrayList(); + if (schemaList.size() == 0) { + schemaList.add(null); + } + for (String s : schemaList) { + rs = getMetaData().getTables(null, s, null, new String[] { "TABLE" }); + while (rs.next()) { + String t = rs.getString("TABLE_NAME"); + if (t.charAt(0) == '"') { + t = t.substring(1); + } + if (t.charAt(t.length() - 1) == '"') { + t = t.substring(0, t.length() - 1); + } + if (!t.equalsIgnoreCase(iciqlTables)) { + tables.add(new TableInspector(s, t, dateTimeClass)); + } + } + } + + if (StringUtils.isNullOrEmpty(schema) && StringUtils.isNullOrEmpty(table)) { + // all schemas and tables + return tables; + } + // schema subset OR table subset OR exact match + List matches = Utils.newArrayList(); + for (TableInspector t : tables) { + if (t.matches(schema, table)) { + matches.add(t); + } + } + if (matches.size() == 0) { + throw new IciqlException(MessageFormat.format("Failed to find schema={0} table={1}", + schema == null ? "" : schema, table == null ? "" : table)); + } + return matches; + } finally { + JdbcUtils.closeSilently(rs); + } + } + +} diff --git a/src/main/java/com/iciql/DbUpgrader.java b/src/main/java/com/iciql/DbUpgrader.java new file mode 100644 index 0000000..1303f4e --- /dev/null +++ b/src/main/java/com/iciql/DbUpgrader.java @@ -0,0 +1,81 @@ +/* + * 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 com.iciql.Iciql.IQVersion; + +/** + * Interface which defines a class to handle table changes based on model + * versions. An implementation of DbUpgrader must be annotated with the + * IQDatabase annotation, which defines the expected database version + * number. + */ +public interface DbUpgrader { + + /** + * Defines method interface to handle database upgrades. This method is only + * called if your DbUpgrader implementation is annotated with + * IQDatabase. + * + * @param db + * the database + * @param fromVersion + * the old version + * @param toVersion + * the new version + * @return true for successful upgrade. If the upgrade is successful, the + * version registry is automatically updated. + */ + boolean upgradeDatabase(Db db, int fromVersion, int toVersion); + + /** + * Defines method interface to handle table upgrades. + * + * @param db + * the database + * @param schema + * the schema + * @param table + * the table + * @param fromVersion + * the old version + * @param toVersion + * the new version + * @return true for successful upgrade. If the upgrade is successful, the + * version registry is automatically updated. + */ + boolean upgradeTable(Db db, String schema, String table, int fromVersion, int toVersion); + + /** + * The default database upgrader. It throws runtime exception instead of + * handling upgrade requests. + */ + @IQVersion(0) + public static class DefaultDbUpgrader implements DbUpgrader { + + public boolean upgradeDatabase(Db db, int fromVersion, int toVersion) { + throw new IciqlException("Please provide your own DbUpgrader implementation."); + } + + public boolean upgradeTable(Db db, String schema, String table, int fromVersion, int toVersion) { + throw new IciqlException("Please provide your own DbUpgrader implementation."); + } + + } + +} diff --git a/src/main/java/com/iciql/DbVersion.java b/src/main/java/com/iciql/DbVersion.java new file mode 100644 index 0000000..6270e14 --- /dev/null +++ b/src/main/java/com/iciql/DbVersion.java @@ -0,0 +1,55 @@ +/* + * 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 com.iciql.Iciql.IQColumn; +import com.iciql.Iciql.IQTable; + +/** + * A system table to track database and table versions. + */ +@IQTable(name = "iq_versions", primaryKey = { "schemaName", "tableName" }, memoryTable = true) +public class DbVersion { + + @IQColumn(length = 255) + String schemaName = ""; + + @IQColumn(length = 255) + String tableName = ""; + + @IQColumn + Integer version; + + public DbVersion() { + // nothing to do + } + + /** + * Constructor for defining a version entry. Both the schema and the table + * are empty strings, which means this is the row for the 'database'. + * + * @param version + * the database version + */ + public DbVersion(int version) { + this.schemaName = ""; + this.tableName = ""; + this.version = version; + } + +} diff --git a/src/main/java/com/iciql/Define.java b/src/main/java/com/iciql/Define.java new file mode 100644 index 0000000..1810a4b --- /dev/null +++ b/src/main/java/com/iciql/Define.java @@ -0,0 +1,145 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * Copyright 2012 Frédéric Gaillard. + * + * 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 com.iciql.Iciql.IndexType; + +/** + * This class provides utility methods to define primary keys, indexes, and set + * the name of the table. + */ + +public class Define { + + private static TableDefinition currentTableDefinition; + private static Iciql currentTable; + + public static void skipCreate() { + checkInDefine(); + currentTableDefinition.defineSkipCreate(); + } + + public static void index(IndexType type, Object... columns) { + checkInDefine(); + currentTableDefinition.defineIndex(null, type, columns); + } + + public static void index(String name, IndexType type, Object... columns) { + checkInDefine(); + currentTableDefinition.defineIndex(name, type, columns); + } + + public static void constraintUnique(String name, Object... columns) { + checkInDefine(); + currentTableDefinition.defineConstraintUnique(name, columns); + } + + /* + * The variable argument type Object can't be used twice :-) + */ +// public static void constraintForeignKey(String name, String refTableName, +// ConstraintDeleteType deleteType, ConstraintUpdateType updateType, +// ConstraintDeferrabilityType deferrabilityType, Object... columns, Object... refColumns) { +// checkInDefine(); +// currentTableDefinition.defineForeignKey(name, columns, refTableName, Columns, deleteType, updateType, deferrabilityType); +// } + + public static void primaryKey(Object... columns) { + checkInDefine(); + currentTableDefinition.definePrimaryKey(columns); + } + + public static void schemaName(String schemaName) { + checkInDefine(); + currentTableDefinition.defineSchemaName(schemaName); + } + + public static void tableName(String tableName) { + checkInDefine(); + currentTableDefinition.defineTableName(tableName); + } + + public static void viewTableName(String viewTableName) { + checkInDefine(); + currentTableDefinition.defineViewTableName(viewTableName); + } + + public static void memoryTable() { + checkInDefine(); + currentTableDefinition.defineMemoryTable(); + } + + public static void columnName(Object column, String columnName) { + checkInDefine(); + currentTableDefinition.defineColumnName(column, columnName); + } + + public static void autoIncrement(Object column) { + checkInDefine(); + currentTableDefinition.defineAutoIncrement(column); + } + + public static void length(Object column, int length) { + checkInDefine(); + currentTableDefinition.defineLength(column, length); + } + + public static void scale(Object column, int scale) { + checkInDefine(); + currentTableDefinition.defineScale(column, scale); + } + + public static void trim(Object column) { + checkInDefine(); + currentTableDefinition.defineTrim(column); + } + + public static void nullable(Object column, boolean isNullable) { + checkInDefine(); + currentTableDefinition.defineNullable(column, isNullable); + } + + public static void defaultValue(Object column, String defaultValue) { + checkInDefine(); + currentTableDefinition.defineDefaultValue(column, defaultValue); + } + + public static void constraint(Object column, String constraint) { + checkInDefine(); + currentTableDefinition.defineConstraint(column, constraint); + } + + static synchronized void define(TableDefinition tableDefinition, Iciql table) { + currentTableDefinition = tableDefinition; + currentTable = table; + tableDefinition.mapObject(table); + table.defineIQ(); + currentTable = null; + currentTableDefinition = null; + } + + private static void checkInDefine() { + if (currentTable == null) { + throw new IciqlException("This method may only be called " + + "from within the define() method, and the define() method " + + "is called by the framework."); + } + } + +} diff --git a/src/main/java/com/iciql/Filter.java b/src/main/java/com/iciql/Filter.java new file mode 100644 index 0000000..99dbdc3 --- /dev/null +++ b/src/main/java/com/iciql/Filter.java @@ -0,0 +1,25 @@ +/* + * 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; + +/** + * Represents the WHERE clause of a query. + */ +public interface Filter { + boolean where(); +} diff --git a/src/main/java/com/iciql/Function.java b/src/main/java/com/iciql/Function.java new file mode 100644 index 0000000..3faddb7 --- /dev/null +++ b/src/main/java/com/iciql/Function.java @@ -0,0 +1,149 @@ +/* + * 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 com.iciql.util.Utils; + +/** + * This class provides static methods that represents common SQL functions. + */ +public class Function implements Token { + + // must be a new instance + private static final Long COUNT_STAR = new Long(0); + + protected Object[] x; + private String name; + + protected Function(String name, Object... x) { + this.name = name; + this.x = x; + } + + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL(name).appendSQL("("); + int i = 0; + for (Object o : x) { + if (i++ > 0) { + stat.appendSQL(","); + } + query.appendSQL(stat, null, o); + } + stat.appendSQL(")"); + } + + public static Long count() { + return COUNT_STAR; + } + + public static Integer length(Object x) { + return Db.registerToken(Utils.newObject(Integer.class), new Function("LENGTH", x)); + } + + @SuppressWarnings("unchecked") + public static T sum(T x) { + return (T) Db.registerToken(Utils.newObject(x.getClass()), new Function("SUM", x)); + } + + public static Long count(Object x) { + return Db.registerToken(Utils.newObject(Long.class), new Function("COUNT", x)); + } + + public static Boolean isNull(Object x) { + return Db.registerToken(Utils.newObject(Boolean.class), new Function("", x) { + public void appendSQL(SQLStatement stat, Query query) { + query.appendSQL(stat, null, x[0]); + stat.appendSQL(" IS NULL"); + } + }); + } + + public static Boolean isNotNull(Object x) { + return Db.registerToken(Utils.newObject(Boolean.class), new Function("", x) { + public void appendSQL(SQLStatement stat, Query query) { + query.appendSQL(stat, null, x[0]); + stat.appendSQL(" IS NOT NULL"); + } + }); + } + + public static Boolean not(Boolean x) { + return Db.registerToken(Utils.newObject(Boolean.class), new Function("", x) { + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("NOT "); + query.appendSQL(stat, null, x[0]); + } + }); + } + + public static Boolean or(Boolean... x) { + return Db.registerToken(Utils.newObject(Boolean.class), new Function("", (Object[]) x) { + public void appendSQL(SQLStatement stat, Query query) { + int i = 0; + for (Object o : x) { + if (i++ > 0) { + stat.appendSQL(" OR "); + } + query.appendSQL(stat, null, o); + } + } + }); + } + + public static Boolean and(Boolean... x) { + return Db.registerToken(Utils.newObject(Boolean.class), new Function("", (Object[]) x) { + public void appendSQL(SQLStatement stat, Query query) { + int i = 0; + for (Object o : x) { + if (i++ > 0) { + stat.appendSQL(" AND "); + } + query.appendSQL(stat, null, o); + } + } + }); + } + + @SuppressWarnings("unchecked") + public static X min(X x) { + Class clazz = (Class) x.getClass(); + X o = Utils.newObject(clazz); + return Db.registerToken(o, new Function("MIN", x)); + } + + @SuppressWarnings("unchecked") + public static X max(X x) { + Class clazz = (Class) x.getClass(); + X o = Utils.newObject(clazz); + return Db.registerToken(o, new Function("MAX", x)); + } + + public static Boolean like(String x, String pattern) { + Boolean o = Utils.newObject(Boolean.class); + return Db.registerToken(o, new Function("LIKE", x, pattern) { + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("("); + query.appendSQL(stat, null, x[0]); + stat.appendSQL(" LIKE "); + query.appendSQL(stat, x[0], x[1]); + stat.appendSQL(")"); + } + }); + } + +} diff --git a/src/main/java/com/iciql/Iciql.java b/src/main/java/com/iciql/Iciql.java new file mode 100644 index 0000000..9f73ffa --- /dev/null +++ b/src/main/java/com/iciql/Iciql.java @@ -0,0 +1,731 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * Copyright 2012 Frédéric Gaillard. + * + * 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.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A class that implements this interface can be used as a database table. + *

+ * You may implement the Table interface on your model object and optionally use + * IQColumn annotations (which imposes a compile-time and runtime-dependency on + * iciql), or may choose to use the IQTable and IQColumn annotations only (which + * imposes a compile-time and runtime-dependency on this file only). + *

+ * If a class is annotated with IQTable and at the same time implements Table, + * the define() method is not called. + *

+ * Fully Supported Data Types: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
All Databases
java.lang.StringVARCHAR (length > 0) or CLOB (length == 0)
java.lang.BooleanBIT
java.lang.ByteTINYINT
java.lang.ShortSMALLINT
java.lang.IntegerINT
java.lang.LongBIGINT
java.lang.FloatREAL
java.lang.DoubleDOUBLE
java.math.BigDecimalDECIMAL (length == 0)
+ * DECIMAL(length, scale) (length > 0)
java.sql.DateDATE
java.sql.TimeTIME
java.sql.TimestampTIMESTAMP
java.util.DateTIMESTAMP
java.lang.Enum.name()VARCHAR (length > 0) or CLOB (length == 0)
+ * EnumType.NAME
java.lang.Enum.ordinal()INT
+ * EnumType.ORDINAL
java.lang.Enum implements
+ * com.iciql.Iciql.EnumID.enumId()
INT
+ * EnumType.ENUMID
H2 Databases
java.util.UUIDUUID
+ *

+ * Partially Supported Data Types: + *

+ * The following data types can be mapped to columns for all general statements + * BUT these field types may not be used to specify compile-time clauses or + * constraints. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
byte []BLOB
booleanBIT
byteTINYINT
shortSMALLINT
intINT
longBIGINT
floatREAL
doubleDOUBLE
+ *

+ * Table and field mapping: by default, the mapped table name is the class name + * and the public fields are reflectively mapped, by their name, to columns. As + * an alternative, you may specify both the table and column definition by + * annotations. + *

+ * Table Interface: you may set additional parameters such as table name, + * primary key, and indexes in the define() method. + *

+ * Annotations: you may use the annotations with or without implementing the + * Table interface. The annotations allow you to decouple your model completely + * from iciql other than this file. + *

+ * Automatic model generation: you may automatically generate model classes as + * strings with the Db and DbInspector objects: + * + *

+ * Db db = Db.open("jdbc:h2:mem:", "sa", "sa");
+ * DbInspector inspector = new DbInspector(db);
+ * List<String> models =
+ *         inspector.generateModel(schema, table, packageName,
+ *         annotateSchema, trimStrings)
+ * 
+ * + * Or you may use the GenerateModels tool to generate and save your classes to + * the file system: + * + *
+ * java -jar iciql.jar
+ *      -url "jdbc:h2:mem:"
+ *      -user sa -password sa -schema schemaName -table tableName
+ *      -package packageName -folder destination
+ *      -annotateSchema false -trimStrings true
+ * 
+ * + * Model validation: you may validate your model class with DbInspector object. + * The DbInspector will report errors, warnings, and suggestions: + * + *
+ * Db db = Db.open("jdbc:h2:mem:", "sa", "sa");
+ * DbInspector inspector = new DbInspector(db);
+ * List<Validation> remarks = inspector.validateModel(new MyModel(), throwOnError);
+ * for (Validation remark : remarks) {
+ * 	System.out.println(remark);
+ * }
+ * 
+ */ +public interface Iciql { + + /** + * An annotation for an iciql version. + *

+ * + * @IQVersion(1) + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface IQVersion { + + /** + * If set to a non-zero value, iciql maintains a "iq_versions" table + * within your database. The version number is used to call to a + * registered DbUpgrader implementation to perform relevant ALTER + * statements. Default: 0. You must specify a DbUpgrader on your Db + * object to use this parameter. + */ + int value() default 0; + + } + + /** + * An annotation for a schema. + *

+ * + * @IQSchema("PUBLIC") + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface IQSchema { + + /** + * The schema may be optionally specified. Default: unspecified. + */ + String value() default ""; + + } + + /** + * Enumeration defining the four index types. + */ + public static enum IndexType { + STANDARD, UNIQUE, HASH, UNIQUE_HASH; + } + + /** + * An index annotation. + *

+ *

    + *
  • @IQIndex("name") + *
  • @IQIndex({"street", "city"}) + *
  • @IQIndex(name="streetidx", value={"street", "city"}) + *
  • @IQIndex(name="addressidx", type=IndexType.UNIQUE, + * value={"house_number", "street", "city"}) + *
+ */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface IQIndex { + + /** + * Index name. If null or empty, iciql will generate one. + */ + String name() default ""; + + /** + * Type of the index. + *
    + *
  • com.iciql.iciql.IndexType.STANDARD + *
  • com.iciql.iciql.IndexType.UNIQUE + *
  • com.iciql.iciql.IndexType.HASH + *
  • com.iciql.iciql.IndexType.UNIQUE_HASH + *
+ * + * HASH indexes may only be valid for single column indexes. + * + */ + IndexType type() default IndexType.STANDARD; + + /** + * Columns to include in index. + *
    + *
  • single column index: value = "id" + *
  • multiple column index: value = { "id", "name", "date" } + *
+ */ + String[] value() default {}; + } + + /** + * Enumeration defining the ON DELETE actions. + */ + public static enum ConstraintDeleteType { + UNSET, CASCADE, RESTRICT, SET_NULL, NO_ACTION, SET_DEFAULT; + } + + /** + * Enumeration defining the ON UPDATE actions. + */ + public static enum ConstraintUpdateType { + UNSET, CASCADE, RESTRICT, SET_NULL, NO_ACTION, SET_DEFAULT; + } + + /** + * Enumeration defining the deferrability. + */ + public static enum ConstraintDeferrabilityType { + UNSET, DEFERRABLE_INITIALLY_DEFERRED, DEFERRABLE_INITIALLY_IMMEDIATE, NOT_DEFERRABLE; + } + + /** + * A foreign key constraint annotation. + *

+ *

    + *
  • @IQContraintForeignKey( + * foreignColumns = { "idaccount"}, + * referenceName = "account", + * referenceColumns = { "id" }, + * deleteType = ConstrainDeleteType.CASCADE, + * updateType = ConstraintUpdateType.NO_ACTION ) + *
+ * Note : reference columns should have a unique constraint defined in referenceName table, + * some database used to define a unique index instead of a unique constraint + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface IQContraintForeignKey { + + /** + * Constraint name. If null or empty, iciql will generate one. + */ + String name() default ""; + + /** + * Type of the action on delete, default to unspecified. + *
    + *
  • com.iciql.iciql.ConstrainDeleteType.CASCADE + *
  • com.iciql.iciql.ConstrainDeleteType.RESTRICT + *
  • com.iciql.iciql.ConstrainDeleteType.SET_NULL + *
  • com.iciql.iciql.ConstrainDeleteType.NO_ACTION + *
  • com.iciql.iciql.ConstrainDeleteType.SET_DEFAULT + *
+ */ + ConstraintDeleteType deleteType() default ConstraintDeleteType.UNSET; + + /** + * Type of the action on update, default to unspecified. + *
    + *
  • com.iciql.iciql.ConstrainUpdateType.CASCADE + *
  • com.iciql.iciql.ConstrainUpdateType.RESTRICT + *
  • com.iciql.iciql.ConstrainUpdateType.SET_NULL + *
  • com.iciql.iciql.ConstrainUpdateType.NO_ACTION + *
  • com.iciql.iciql.ConstrainUpdateType.SET_DEFAULT + *
+ */ + ConstraintUpdateType updateType() default ConstraintUpdateType.UNSET; + + /** + * Type of the deferrability mode, default to unspecified + *
    + *
  • com.iciql.iciql.ConstrainUpdateType.CASCADE + *
  • ConstraintDeferrabilityType.DEFERRABLE_INITIALLY_DEFERRED + *
  • ConstraintDeferrabilityType.DEFERRABLE_INITIALLY_IMMEDIATE + *
  • ConstraintDeferrabilityType.NOT_DEFERRABLE + *
+ */ + ConstraintDeferrabilityType deferrabilityType() default ConstraintDeferrabilityType.UNSET; + + /** + * The source table for the columns defined as foreign. + */ + String tableName() default ""; + + /** + * Columns defined as 'foreign'. + *
    + *
  • single column : foreignColumns = "id" + *
  • multiple column : foreignColumns = { "id", "name", "date" } + *
+ */ + String[] foreignColumns() default {}; + + /** + * The reference table for the columns defined as references. + */ + String referenceName() default ""; + + /** + * Columns defined as 'references'. + *
    + *
  • single column : referenceColumns = "id" + *
  • multiple column : referenceColumns = { "id", "name", "date" } + *
+ */ + String[] referenceColumns() default {}; + } + + /** + * Annotation to specify multiple foreign keys constraints. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface IQContraintsForeignKey { + IQContraintForeignKey[] value() default {}; + } + + /** + * A unique constraint annotation. + *

+ *

    + *
  • @IQContraintUnique(uniqueColumns = { "street", "city" }) + *
  • @IQContraintUnique(name="streetconstraint", uniqueColumns = { "street", "city" }) + *
+ */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface IQContraintUnique { + + /** + * Constraint name. If null or empty, iciql will generate one. + */ + String name() default ""; + + /** + * Columns defined as 'unique'. + *
    + *
  • single column : uniqueColumns = "id" + *
  • multiple column : uniqueColumns = { "id", "name", "date" } + *
+ */ + String[] uniqueColumns() default {}; + + } + + /** + * Annotation to specify multiple unique constraints. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface IQContraintsUnique { + IQContraintUnique[] value() default {}; + } + + /** + * Annotation to define a view. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface IQView { + + /** + * The view name. If not specified the class name is used as the view + * name. + *

+ * The view name may still be overridden in the define() method if the + * model class is not annotated with IQView. Default: unspecified. + */ + String name() default ""; + + /** + * The source table for the view. + *

+ * The view name may still be overridden in the define() method if the + * model class is not annotated with IQView. Default: unspecified. + */ + String tableName() default ""; + + /** + * The inherit columns allows this model class to inherit columns from + * its super class. Any IQTable annotation present on the super class is + * ignored. Default: false. + */ + boolean inheritColumns() default false; + + /** + * Whether or not iciql tries to create the view. Default: + * true. + */ + boolean create() default true; + + /** + * If true, only fields that are explicitly annotated as IQColumn are + * mapped. Default: true. + */ + boolean annotationsOnly() default true; + } + + /** + * String snippet defining SQL constraints for a field. Use "this" as + * a placeholder for the column name. "this" will be substituted at + * runtime. + *

+ * IQConstraint("this > 2 AND this <= 7") + *

+ * This snippet may still be overridden in the define() method if the + * model class is not annotated with IQTable or IQView. Default: unspecified. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface IQConstraint { + + String value() default ""; + } + + /** + * Annotation to specify multiple indexes. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface IQIndexes { + IQIndex[] value() default {}; + } + + /** + * Annotation to define a table. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface IQTable { + + /** + * The table name. If not specified the class name is used as the table + * name. + *

+ * The table name may still be overridden in the define() method if the + * model class is not annotated with IQTable. Default: unspecified. + */ + String name() default ""; + + /** + * The primary key may be optionally specified. If it is not specified, + * then no primary key is set by the IQTable annotation. You may specify + * a composite primary key. + *

    + *
  • single column primaryKey: value = "id" + *
  • compound primary key: value = { "id", "name" } + *
+ * The primary key may still be overridden in the define() method if the + * model class is not annotated with IQTable. Default: unspecified. + */ + String[] primaryKey() default {}; + + /** + * The inherit columns allows this model class to inherit columns from + * its super class. Any IQTable annotation present on the super class is + * ignored. Default: false. + */ + boolean inheritColumns() default false; + + /** + * Whether or not iciql tries to create the table and indexes. Default: + * true. + */ + boolean create() default true; + + /** + * If true, only fields that are explicitly annotated as IQColumn are + * mapped. Default: true. + */ + boolean annotationsOnly() default true; + + /** + * If true, this table is created as a memory table where data is + * persistent, but index data is kept in main memory. Valid only for H2 + * and HSQL databases. Default: false. + */ + boolean memoryTable() default false; + } + + /** + * Annotation to define a column. Annotated fields may have any scope + * (however, the JVM may raise a SecurityException if the SecurityManager + * doesn't allow iciql to access the field.) + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface IQColumn { + + /** + * If not specified, the field name is used as the column name. Default: + * the field name. + */ + String name() default ""; + + /** + * This column is the primary key. Default: false. + */ + boolean primaryKey() default false; + + /** + * The column is created with a sequence as the default value. Default: + * false. + */ + boolean autoIncrement() default false; + + /** + * Length is used to define the length of a VARCHAR column or to define + * the precision of a DECIMAL(precision, scale) expression. + *

+ * If larger than zero, it is used during the CREATE TABLE phase. For + * string values it may also be used to prevent database exceptions on + * INSERT and UPDATE statements (see trim). + *

+ * Any length set in define() may override this annotation setting if + * the model class is not annotated with IQTable. Default: 0. + */ + int length() default 0; + + /** + * Scale is used during the CREATE TABLE phase to define the scale of a + * DECIMAL(precision, scale) expression. + *

+ * Any scale set in define() may override this annotation setting if the + * model class is not annotated with IQTable. Default: 0. + */ + int scale() default 0; + + /** + * If true, iciql will automatically trim the string if it exceeds + * length (value.substring(0, length)). Default: false. + */ + boolean trim() default false; + + /** + * If false, iciql will set the column NOT NULL during the CREATE TABLE + * phase. Default: true. + */ + boolean nullable() default true; + + /** + * The default value assigned to the column during the CREATE TABLE + * phase. This field could contain a literal single-quoted value, or a + * function call. Empty strings are considered NULL. Examples: + *

    + *
  • defaultValue="" (null) + *
  • defaultValue="CURRENT_TIMESTAMP" + *
  • defaultValue="''" (empty string) + *
  • defaultValue="'0'" + *
  • defaultValue="'1970-01-01 00:00:01'" + *
+ * if the default value is specified, and auto increment is disabled, + * and primary key is disabled, then this value is included in the + * "DEFAULT ..." phrase of a column during the CREATE TABLE process. + *

+ * Alternatively, you may specify a default object value on the field + * and this will be converted to a properly formatted DEFAULT expression + * during the CREATE TABLE process. + *

+ * Default: unspecified (null). + */ + String defaultValue() default ""; + + } + + /** + * Interface for using the EnumType.ENUMID enumeration mapping strategy. + *

+ * Enumerations wishing to use EnumType.ENUMID must implement this + * interface. + */ + public interface EnumId { + int enumId(); + } + + /** + * Enumeration representing how to map a java.lang.Enum to a column. + *

+ *

    + *
  • NAME - name() : string + *
  • ORDINAL - ordinal() : int + *
  • ENUMID - enumId() : int + *
+ * + * @see com.iciql.Iciql.EnumId interface + */ + public enum EnumType { + NAME, ORDINAL, ENUMID; + + public static final EnumType DEFAULT_TYPE = NAME; + } + + /** + * Annotation to define how a java.lang.Enum is mapped to a column. + *

+ * This annotation can be used on: + *

    + *
  • a field instance of an enumeration type + *
  • on the enumeration class declaration + *
+ * If you choose to annotate the class declaration, that will be the default + * mapping strategy for all @IQColumn instances of the enum. This can still + * be overridden for an individual field by specifying the IQEnum + * annotation. + *

+ * The default mapping is by NAME. + * + *

+	 * IQEnum(EnumType.NAME)
+	 * 
+ * + * A string mapping will generate either a VARCHAR, if IQColumn.length > 0 + * or a TEXT column if IQColumn.length == 0 + * + */ + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.FIELD, ElementType.TYPE }) + public @interface IQEnum { + EnumType value() default EnumType.NAME; + } + + /** + * Annotation to define an ignored field. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface IQIgnore{ + } + + /** + * This method is called to let the table define the primary key, indexes, + * and the table name. + */ + void defineIQ(); +} diff --git a/src/main/java/com/iciql/IciqlException.java b/src/main/java/com/iciql/IciqlException.java new file mode 100644 index 0000000..3f27b73 --- /dev/null +++ b/src/main/java/com/iciql/IciqlException.java @@ -0,0 +1,177 @@ +/* + * Copyright 2011 James Moger. + * Copyright 2012 Frédéric Gaillard + * + * 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.sql.SQLException; +import java.text.MessageFormat; +import java.util.regex.Pattern; + +/** + * Iciql wraps all exceptions with this class. + */ +public class IciqlException extends RuntimeException { + + public static final int CODE_UNMAPPED_FIELD = 1; + public static final int CODE_DUPLICATE_KEY = 2; + public static final int CODE_OBJECT_NOT_FOUND = 3; + public static final int CODE_OBJECT_ALREADY_EXISTS = 4; + public static final int CODE_CONSTRAINT_VIOLATION = 5; + public static final int CODE_UNCHARACTERIZED = 6; + + private static final String TOKEN_UNMAPPED_FIELD = "\\? (=|\\>|\\<|\\<\\>|!=|\\>=|\\<=|LIKE|BETWEEN) \\?"; + + private static final long serialVersionUID = 1L; + + private String sql; + + private int iciqlCode; + + public IciqlException(Throwable t) { + super(t.getMessage(), t); + configureCode(t); + } + + public IciqlException(String message, Object... parameters) { + super(parameters.length > 0 ? MessageFormat.format(message, parameters) : message); + } + + public IciqlException(Throwable t, String message, Object... parameters) { + super(parameters.length > 0 ? MessageFormat.format(message, parameters) : message, t); + configureCode(t); + } + + public static void checkUnmappedField(String sql) { + if (Pattern.compile(IciqlException.TOKEN_UNMAPPED_FIELD).matcher(sql).find()) { + IciqlException e = new IciqlException("unmapped field in statement!"); + e.sql = sql; + e.iciqlCode = CODE_UNMAPPED_FIELD; + throw e; + } + } + + public static IciqlException fromSQL(String sql, Throwable t) { + if (Pattern.compile(TOKEN_UNMAPPED_FIELD).matcher(sql).find()) { + IciqlException e = new IciqlException(t, "unmapped field in statement!"); + e.sql = sql; + e.iciqlCode = CODE_UNMAPPED_FIELD; + return e; + } else { + IciqlException e = new IciqlException(t, t.getMessage()); + e.sql = sql; + return e; + } + } + + public void setSQL(String sql) { + this.sql = sql; + } + + public String getSQL() { + return sql; + } + + public int getIciqlCode() { + return iciqlCode; + } + + private void configureCode(Throwable t) { + if (t == null) { + return; + } + if (t instanceof SQLException) { + // http://developer.mimer.com/documentation/html_92/Mimer_SQL_Mobile_DocSet/App_Return_Codes2.html + SQLException s = (SQLException) t; + String state = s.getSQLState(); + if ("23000".equals(state)) { + // MySQL duplicate primary key on insert + iciqlCode = CODE_DUPLICATE_KEY; + if (s.getErrorCode() == 1217) { + iciqlCode = CODE_CONSTRAINT_VIOLATION; + } + } else if ("23505".equals(state)) { + // Derby duplicate primary key on insert + iciqlCode = CODE_DUPLICATE_KEY; + } else if ("42000".equals(state)) { + // MySQL duplicate unique index value on insert + iciqlCode = CODE_DUPLICATE_KEY; + } else if ("42Y07".equals(state)) { + // Derby schema not found + iciqlCode = CODE_OBJECT_NOT_FOUND; + } else if ("42X05".equals(state)) { + // Derby table not found + iciqlCode = CODE_OBJECT_NOT_FOUND; + } else if ("42Y55".equals(state)) { + // Derby table not found + iciqlCode = CODE_OBJECT_NOT_FOUND; + } else if ("42S02".equals(state)) { + // H2 table not found + iciqlCode = CODE_OBJECT_NOT_FOUND; + } else if ("42501".equals(state)) { + // HSQL table not found + iciqlCode = CODE_OBJECT_NOT_FOUND; + } else if ("42P01".equals(state)) { + // PostgreSQL table not found + iciqlCode = CODE_OBJECT_NOT_FOUND; + } else if ("X0X05".equals(state)) { + // Derby view/table not found exists + iciqlCode = CODE_OBJECT_NOT_FOUND; + } else if ("X0Y32".equals(state)) { + // Derby table already exists + iciqlCode = CODE_OBJECT_ALREADY_EXISTS; + } else if ("42P07".equals(state)) { + // PostgreSQL table or index already exists + iciqlCode = CODE_OBJECT_ALREADY_EXISTS; + } else if ("42S01".equals(state)) { + // MySQL view already exists + iciqlCode = CODE_OBJECT_ALREADY_EXISTS; + } else if ("42S11".equals(state)) { + // H2 index already exists + iciqlCode = CODE_OBJECT_ALREADY_EXISTS; + } else if ("42504".equals(state)) { + // HSQL index already exists + iciqlCode = CODE_OBJECT_ALREADY_EXISTS; + } else if ("2BP01".equals(state)) { + // PostgreSQL constraint violation + iciqlCode = CODE_CONSTRAINT_VIOLATION; + } else if ("42533".equals(state)) { + // HSQL constraint violation + iciqlCode = CODE_CONSTRAINT_VIOLATION; + } else if ("X0Y25".equals(state)) { + // Derby constraint violation + iciqlCode = CODE_CONSTRAINT_VIOLATION; + } else { + // uncharacterized SQL code, we can always rely on iciqlCode != 0 in IciqlException + iciqlCode = s.getErrorCode() == 0 ? CODE_UNCHARACTERIZED : s.getErrorCode(); + } + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getName()); + String message = getLocalizedMessage(); + if (message != null) { + sb.append(": ").append(message); + } + if (sql != null) { + sb.append('\n').append(sql); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/iciql/ModelUtils.java b/src/main/java/com/iciql/ModelUtils.java new file mode 100644 index 0000000..56e6440 --- /dev/null +++ b/src/main/java/com/iciql/ModelUtils.java @@ -0,0 +1,499 @@ +/* + * 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 static com.iciql.util.StringUtils.isNullOrEmpty; + +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.text.DateFormat; +import java.text.MessageFormat; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.regex.Pattern; + +import com.iciql.TableDefinition.FieldDefinition; +import com.iciql.util.StringUtils; + +/** + * Utility methods for models related to type mapping, default value validation, + * and class or field name creation. + */ +class ModelUtils { + + /** + * The list of supported data types. It is used by the runtime mapping for + * CREATE statements. + */ + private static final Map, String> SUPPORTED_TYPES = new HashMap, String>(); + + static { + Map, String> m = SUPPORTED_TYPES; + m.put(String.class, "VARCHAR"); + m.put(Boolean.class, "BOOLEAN"); + m.put(Byte.class, "TINYINT"); + m.put(Short.class, "SMALLINT"); + m.put(Integer.class, "INT"); + m.put(Long.class, "BIGINT"); + m.put(Float.class, "REAL"); + m.put(Double.class, "DOUBLE"); + m.put(BigDecimal.class, "DECIMAL"); + m.put(java.sql.Timestamp.class, "TIMESTAMP"); + m.put(java.util.Date.class, "TIMESTAMP"); + m.put(java.sql.Date.class, "DATE"); + m.put(java.sql.Time.class, "TIME"); + m.put(byte[].class, "BLOB"); + m.put(UUID.class, "UUID"); + + // map primitives + m.put(boolean.class, m.get(Boolean.class)); + m.put(byte.class, m.get(Byte.class)); + m.put(short.class, m.get(Short.class)); + m.put(int.class, m.get(Integer.class)); + m.put(long.class, m.get(Long.class)); + m.put(float.class, m.get(Float.class)); + m.put(double.class, m.get(Double.class)); + } + + /** + * Convert SQL type aliases to the list of supported types. This map is used + * by generation and validation. + */ + private static final Map SQL_TYPES = new HashMap(); + + static { + Map m = SQL_TYPES; + m.put("CHAR", "VARCHAR"); + m.put("CHARACTER", "VARCHAR"); + m.put("NCHAR", "VARCHAR"); + m.put("VARCHAR_CASESENSITIVE", "VARCHAR"); + m.put("VARCHAR_IGNORECASE", "VARCHAR"); + m.put("LONGVARCHAR", "VARCHAR"); + m.put("VARCHAR2", "VARCHAR"); + m.put("NVARCHAR", "VARCHAR"); + m.put("NVARCHAR2", "VARCHAR"); + m.put("TEXT", "VARCHAR"); + m.put("NTEXT", "VARCHAR"); + m.put("TINYTEXT", "VARCHAR"); + m.put("MEDIUMTEXT", "VARCHAR"); + m.put("LONGTEXT", "VARCHAR"); + m.put("CLOB", "VARCHAR"); + m.put("NCLOB", "VARCHAR"); + + // logic + m.put("BIT", "BOOLEAN"); + m.put("BOOL", "BOOLEAN"); + + // numeric + m.put("BYTE", "TINYINT"); + m.put("INT2", "SMALLINT"); + m.put("YEAR", "SMALLINT"); + m.put("INTEGER", "INT"); + m.put("MEDIUMINT", "INT"); + m.put("INT4", "INT"); + m.put("SIGNED", "INT"); + m.put("INT8", "BIGINT"); + m.put("IDENTITY", "BIGINT"); + m.put("SERIAL", "INT"); + m.put("BIGSERIAL", "BIGINT"); + + // decimal + m.put("NUMBER", "DECIMAL"); + m.put("DEC", "DECIMAL"); + m.put("NUMERIC", "DECIMAL"); + m.put("FLOAT", "DOUBLE"); + m.put("FLOAT4", "DOUBLE"); + m.put("FLOAT8", "DOUBLE"); + m.put("DOUBLE PRECISION", "DOUBLE"); + + // date + m.put("DATETIME", "TIMESTAMP"); + m.put("SMALLDATETIME", "TIMESTAMP"); + + // binary types + m.put("TINYBLOB", "BLOB"); + m.put("MEDIUMBLOB", "BLOB"); + m.put("LONGBLOB", "BLOB"); + m.put("IMAGE", "BLOB"); + m.put("OID", "BLOB"); + } + + private static final List KEYWORDS = Arrays.asList("abstract", "assert", "boolean", "break", + "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", + "enum", "extends", "final", "finally", "float", "for", "goto", "if", "implements", "import", + "instanceof", "int", "interface", "long", "native", "new", "package", "private", "protected", + "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", + "throw", "throws", "transient", "try", "void", "volatile", "while", "false", "null", "true"); + + /** + * Returns a SQL type mapping for a Java class. + * + * @param fieldDef + * the field to map + * @return + */ + static String getDataType(FieldDefinition fieldDef) { + Class fieldClass = fieldDef.field.getType(); + if (fieldClass.isEnum()) { + switch (fieldDef.enumType) { + case ORDINAL: + case ENUMID: + return "INT"; + case NAME: + default: + return "VARCHAR"; + } + } + if (SUPPORTED_TYPES.containsKey(fieldClass)) { + return SUPPORTED_TYPES.get(fieldClass); + } + throw new IciqlException("Unsupported type " + fieldClass.getName()); + } + + /** + * Returns the Java class for a given SQL type. + * + * @param sqlType + * @param dateTimeClass + * the preferred date class (java.util.Date or + * java.sql.Timestamp) + * @return + */ + static Class getClassForSqlType(String sqlType, Class dateTimeClass) { + sqlType = sqlType.toUpperCase(); + // XXX dropping "UNSIGNED" or parts like that could be trouble + sqlType = sqlType.split(" ")[0].trim(); + + if (SQL_TYPES.containsKey(sqlType)) { + // convert the sqlType to a standard type + sqlType = SQL_TYPES.get(sqlType); + } + Class mappedClass = null; + for (Class clazz : SUPPORTED_TYPES.keySet()) { + if (clazz.isPrimitive()) { + // do not map from SQL TYPE to primitive type + continue; + } + if (SUPPORTED_TYPES.get(clazz).equalsIgnoreCase(sqlType)) { + mappedClass = clazz; + + break; + } + } + if (mappedClass != null) { + if (mappedClass.equals(java.util.Date.class) || mappedClass.equals(java.sql.Timestamp.class)) { + return dateTimeClass; + } + return mappedClass; + } + return null; + } + + /** + * Tries to create a convert a SQL table name to a camel case class name. + * + * @param tableName + * the SQL table name + * @return the class name + */ + static String convertTableToClassName(String tableName) { + String[] chunks = StringUtils.arraySplit(tableName, '_', false); + StringBuilder className = new StringBuilder(); + for (String chunk : chunks) { + if (chunk.length() == 0) { + // leading or trailing _ + continue; + } + String[] subchunks = StringUtils.arraySplit(chunk, ' ', false); + for (String subchunk : subchunks) { + if (subchunk.length() == 0) { + // leading or trailing space + continue; + } + className.append(Character.toUpperCase(subchunk.charAt(0))); + className.append(subchunk.substring(1).toLowerCase()); + } + } + return className.toString(); + } + + /** + * Ensures that SQL column names don't collide with Java keywords. + * + * @param columnName + * the column name + * @return the Java field name + */ + static String convertColumnToFieldName(String columnName) { + String lower = columnName.toLowerCase(); + if (KEYWORDS.contains(lower)) { + lower += "Value"; + } + return lower; + } + + /** + * Converts a DEFAULT clause value into an object. + * + * @param field + * definition + * @return object + */ + static Object getDefaultValue(FieldDefinition def, Class dateTimeClass) { + Class valueType = getClassForSqlType(def.dataType, dateTimeClass); + if (String.class.isAssignableFrom(valueType)) { + if (StringUtils.isNullOrEmpty(def.defaultValue)) { + // literal default must be specified within single quotes + return null; + } + if (def.defaultValue.charAt(0) == '\'' + && def.defaultValue.charAt(def.defaultValue.length() - 1) == '\'') { + // strip leading and trailing single quotes + return def.defaultValue.substring(1, def.defaultValue.length() - 1).trim(); + } + return def.defaultValue; + } + + if (StringUtils.isNullOrEmpty(def.defaultValue)) { + // can not create object from empty string + return null; + } + + // strip leading and trailing single quotes + String content = def.defaultValue; + if (content.charAt(0) == '\'') { + content = content.substring(1); + } + if (content.charAt(content.length() - 1) == '\'') { + content = content.substring(0, content.length() - 2); + } + + if (StringUtils.isNullOrEmpty(content)) { + // can not create object from empty string + return null; + } + + if (Boolean.class.isAssignableFrom(valueType) || boolean.class.isAssignableFrom(valueType)) { + return Boolean.parseBoolean(content); + } + + if (Number.class.isAssignableFrom(valueType)) { + try { + // delegate to static valueOf() method to parse string + Method m = valueType.getMethod("valueOf", String.class); + return m.invoke(null, content); + } catch (NumberFormatException e) { + throw new IciqlException(e, "Failed to parse {0} as a number!", def.defaultValue); + } catch (Throwable t) { + } + } + + String dateRegex = "[0-9]{1,4}[-/\\.][0-9]{1,2}[-/\\.][0-9]{1,2}"; + String timeRegex = "[0-2]{1}[0-9]{1}:[0-5]{1}[0-9]{1}:[0-5]{1}[0-9]{1}"; + + if (java.sql.Date.class.isAssignableFrom(valueType)) { + // this may be a little loose.... + // 00-00-00 + // 00/00/00 + // 00.00.00 + Pattern pattern = Pattern.compile(dateRegex); + if (pattern.matcher(content).matches()) { + DateFormat df = DateFormat.getDateInstance(); + try { + return df.parse(content); + } catch (Exception e) { + throw new IciqlException(e, "Failed to parse {0} as a date!", def.defaultValue); + } + } + } + + if (java.sql.Time.class.isAssignableFrom(valueType)) { + // 00:00:00 + Pattern pattern = Pattern.compile(timeRegex); + if (pattern.matcher(content).matches()) { + DateFormat df = DateFormat.getTimeInstance(); + try { + return df.parse(content); + } catch (Exception e) { + throw new IciqlException(e, "Failed to parse {0} as a time!", def.defaultValue); + } + } + } + + if (java.util.Date.class.isAssignableFrom(valueType)) { + // this may be a little loose.... + // 00-00-00 00:00:00 + // 00/00/00T00:00:00 + // 00.00.00T00:00:00 + Pattern pattern = Pattern.compile(dateRegex + "." + timeRegex); + if (pattern.matcher(content).matches()) { + DateFormat df = DateFormat.getDateTimeInstance(); + try { + return df.parse(content); + } catch (Exception e) { + throw new IciqlException(e, "Failed to parse {0} as a datetimestamp!", def.defaultValue); + } + } + } + return content; + } + + /** + * Converts the object into a DEFAULT clause value. + * + * @param o + * the default object + * @return the value formatted for a DEFAULT clause + */ + static String formatDefaultValue(Object o) { + Class objectClass = o.getClass(); + String value = null; + if (Number.class.isAssignableFrom(objectClass)) { + // NUMBER + return ((Number) o).toString(); + } else if (Boolean.class.isAssignableFrom(objectClass)) { + // BOOLEAN + return o.toString(); + } else if (java.sql.Date.class.isAssignableFrom(objectClass)) { + // DATE + value = new SimpleDateFormat("yyyy-MM-dd").format((Date) o); + } else if (java.sql.Time.class.isAssignableFrom(objectClass)) { + // TIME + value = new SimpleDateFormat("HH:mm:ss").format((Date) o); + } else if (Date.class.isAssignableFrom(objectClass)) { + // DATETIME + value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((Date) o); + } else if (String.class.isAssignableFrom(objectClass)) { + // STRING + value = o.toString(); + } + if (value == null) { + return "''"; + } + return MessageFormat.format("''{0}''", value); + } + + /** + * Checks the formatting of IQColumn.defaultValue(). + * + * @param defaultValue + * the default value + * @return true if it is + */ + static boolean isProperlyFormattedDefaultValue(String defaultValue) { + if (isNullOrEmpty(defaultValue)) { + return true; + } + Pattern literalDefault = Pattern.compile("'.*'"); + Pattern functionDefault = Pattern.compile("[^'].*[^']"); + return literalDefault.matcher(defaultValue).matches() + || functionDefault.matcher(defaultValue).matches(); + } + + /** + * Checks to see if the default value matches the class. + * + * @param modelClass + * the class + * @param defaultValue + * the value + * @return true if it does + */ + static boolean isValidDefaultValue(Class modelClass, String defaultValue) { + + if (defaultValue == null) { + // NULL + return true; + } + if (defaultValue.trim().length() == 0) { + // NULL (effectively) + return true; + } + + // function / variable + Pattern functionDefault = Pattern.compile("[^'].*[^']"); + if (functionDefault.matcher(defaultValue).matches()) { + // hard to validate this since its in the database + // assume it is good + return true; + } + + // STRING + if (modelClass == String.class) { + Pattern stringDefault = Pattern.compile("'(.|\\n)*'"); + return stringDefault.matcher(defaultValue).matches(); + } + + String dateRegex = "[0-9]{1,4}[-/\\.][0-9]{1,2}[-/\\.][0-9]{1,2}"; + String timeRegex = "[0-2]{1}[0-9]{1}:[0-5]{1}[0-9]{1}:[0-5]{1}[0-9]{1}"; + + // TIMESTAMP + if (modelClass == java.util.Date.class || modelClass == java.sql.Timestamp.class) { + // this may be a little loose.... + // 00-00-00 00:00:00 + // 00/00/00T00:00:00 + // 00.00.00T00:00:00 + Pattern pattern = Pattern.compile("'" + dateRegex + "." + timeRegex + "'"); + return pattern.matcher(defaultValue).matches(); + } + + // DATE + if (modelClass == java.sql.Date.class) { + // this may be a little loose.... + // 00-00-00 + // 00/00/00 + // 00.00.00 + Pattern pattern = Pattern.compile("'" + dateRegex + "'"); + return pattern.matcher(defaultValue).matches(); + } + + // TIME + if (modelClass == java.sql.Time.class) { + // 00:00:00 + Pattern pattern = Pattern.compile("'" + timeRegex + "'"); + return pattern.matcher(defaultValue).matches(); + } + + // NUMBER + if (Number.class.isAssignableFrom(modelClass)) { + // strip single quotes + String unquoted = defaultValue; + if (unquoted.charAt(0) == '\'') { + unquoted = unquoted.substring(1); + } + if (unquoted.charAt(unquoted.length() - 1) == '\'') { + unquoted = unquoted.substring(0, unquoted.length() - 1); + } + + try { + // delegate to static valueOf() method to parse string + Method m = modelClass.getMethod("valueOf", String.class); + m.invoke(null, unquoted); + } catch (NumberFormatException ex) { + return false; + } catch (Throwable t) { + } + } + return true; + } +} diff --git a/src/main/java/com/iciql/OrderExpression.java b/src/main/java/com/iciql/OrderExpression.java new file mode 100644 index 0000000..f450bfb --- /dev/null +++ b/src/main/java/com/iciql/OrderExpression.java @@ -0,0 +1,55 @@ +/* + * 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; + +/** + * An expression to order by in a query. + * + * @param + * the query data type + */ + +class OrderExpression { + private Query query; + private Object expression; + private boolean desc; + private boolean nullsFirst; + private boolean nullsLast; + + OrderExpression(Query query, Object expression, boolean desc, boolean nullsFirst, boolean nullsLast) { + this.query = query; + this.expression = expression; + this.desc = desc; + this.nullsFirst = nullsFirst; + this.nullsLast = nullsLast; + } + + void appendSQL(SQLStatement stat) { + query.appendSQL(stat, null, expression); + if (desc) { + stat.appendSQL(" DESC"); + } + if (nullsLast) { + stat.appendSQL(" NULLS LAST"); + } + if (nullsFirst) { + stat.appendSQL(" NULLS FIRST"); + } + } + +} diff --git a/src/main/java/com/iciql/Query.java b/src/main/java/com/iciql/Query.java new file mode 100644 index 0000000..5dc78a5 --- /dev/null +++ b/src/main/java/com/iciql/Query.java @@ -0,0 +1,947 @@ +/* + * 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.bytecode.ClassReader; +import com.iciql.util.JdbcUtils; +import com.iciql.util.IciqlLogger; +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 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); + 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() { + return select(false).get(0); + } + + public List selectDistinct() { + return select(true); + } + + @SuppressWarnings("unchecked") + public X selectFirst(Z x) { + List list = (List) 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(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(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 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); + } + + private void addParameter(SQLStatement stat, Object alias, Object value) { + if (alias != null && value.getClass().isEnum()) { + SelectColumn col = getColumnByReference(alias); + EnumType type = col.getFieldDefinition().enumType; + Enum anEnum = (Enum) value; + Object y = Utils.convertEnum(anEnum, type); + stat.addParameter(y); + } else { + stat.addParameter(value); + } + } + + void addConditionToken(Token condition) { + conditions.add(condition); + } + + void addUpdateColumnDeclaration(UpdateColumn declaration) { + updateColumnDeclarations.add(declaration); + } + + void appendWhere(SQLStatement stat) { + if (!conditions.isEmpty()) { + stat.appendSQL(" WHERE "); + for (Token token : conditions) { + token.appendSQL(stat, this); + stat.appendSQL(" "); + } + } + } + + 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); + 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); + } + +} diff --git a/src/main/java/com/iciql/QueryBetween.java b/src/main/java/com/iciql/QueryBetween.java new file mode 100644 index 0000000..72d19dc --- /dev/null +++ b/src/main/java/com/iciql/QueryBetween.java @@ -0,0 +1,60 @@ +/* + * 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; + +/** + * This class represents a "between y and z" condition. + * + * @param + * the return type of the query + * @param + * the incomplete condition data type + */ +public class QueryBetween { + + private Query query; + private A x; + private A y; + + /** + * Construct a between condition. + * + * @param query + * the query + * @param x + * the alias + * @param y + * the lower bound of the between condition + */ + public QueryBetween(Query query, A x, A y) { + this.query = query; + this.x = x; + this.y = y; + } + + /** + * Set the upper bound of the between condition. + * + * @param z + * the upper bound of the between condition + * @return the query + */ + public QueryWhere and(A z) { + query.addConditionToken(new Condition(x, y, z, CompareType.BETWEEN)); + return new QueryWhere(query); + } +} diff --git a/src/main/java/com/iciql/QueryCondition.java b/src/main/java/com/iciql/QueryCondition.java new file mode 100644 index 0000000..9613b1b --- /dev/null +++ b/src/main/java/com/iciql/QueryCondition.java @@ -0,0 +1,128 @@ +/* + * 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; + +/** + * This class represents a query with an incomplete condition. + * + * @param + * the return type of the query + * @param + * the incomplete condition data type + */ + +public class QueryCondition { + + private Query query; + private A x; + + QueryCondition(Query query, A x) { + this.query = query; + this.x = x; + } + + public QueryWhere in(SubQuery q) { + query.addConditionToken(new SubQueryCondition(x, q)); + return new QueryWhere(query); + } + + public QueryWhere is(A y) { + query.addConditionToken(new Condition(x, y, CompareType.EQUAL)); + return new QueryWhere(query); + } + + public QueryWhere isNot(A y) { + query.addConditionToken(new Condition(x, y, CompareType.NOT_EQUAL)); + return new QueryWhere(query); + } + + public QueryWhere isNull() { + query.addConditionToken(new Condition(x, null, CompareType.IS_NULL)); + return new QueryWhere(query); + } + + public QueryWhere isNotNull() { + query.addConditionToken(new Condition(x, null, CompareType.IS_NOT_NULL)); + return new QueryWhere(query); + } + + public QueryWhere exceeds(A y) { + query.addConditionToken(new Condition(x, y, CompareType.EXCEEDS)); + return new QueryWhere(query); + } + + public QueryWhere atLeast(A y) { + query.addConditionToken(new Condition(x, y, CompareType.AT_LEAST)); + return new QueryWhere(query); + } + + public QueryWhere lessThan(A y) { + query.addConditionToken(new Condition(x, y, CompareType.LESS_THAN)); + return new QueryWhere(query); + } + + public QueryWhere atMost(A y) { + query.addConditionToken(new Condition(x, y, CompareType.AT_MOST)); + return new QueryWhere(query); + } + + public QueryBetween between(A y) { + return new QueryBetween(query, x, y); + } + + public QueryWhere like(A pattern) { + query.addConditionToken(new Condition(x, pattern, CompareType.LIKE)); + return new QueryWhere(query); + } + + /* + * These method allows you to generate "x=?", "x!=?", etc where conditions. + * Parameter substitution must be done manually later with db.executeQuery. + * This allows for building re-usable SQL string statements from your model + * classes. + */ + public QueryWhere isParameter() { + query.addConditionToken(new RuntimeParameter(x, CompareType.EQUAL)); + return new QueryWhere(query); + } + + public QueryWhere isNotParameter() { + query.addConditionToken(new RuntimeParameter(x, CompareType.NOT_EQUAL)); + return new QueryWhere(query); + } + + public QueryWhere exceedsParameter() { + query.addConditionToken(new RuntimeParameter(x, CompareType.EXCEEDS)); + return new QueryWhere(query); + } + + public QueryWhere lessThanParameter() { + query.addConditionToken(new RuntimeParameter(x, CompareType.LESS_THAN)); + return new QueryWhere(query); + } + + public QueryWhere atMostParameter() { + query.addConditionToken(new RuntimeParameter(x, CompareType.AT_MOST)); + return new QueryWhere(query); + } + + public QueryWhere likeParameter() { + query.addConditionToken(new RuntimeParameter(x, CompareType.LIKE)); + return new QueryWhere(query); + } +} diff --git a/src/main/java/com/iciql/QueryJoin.java b/src/main/java/com/iciql/QueryJoin.java new file mode 100644 index 0000000..6d0484e --- /dev/null +++ b/src/main/java/com/iciql/QueryJoin.java @@ -0,0 +1,75 @@ +/* + * 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; + +/** + * This class represents a query with a join. + */ + +public class QueryJoin { + + private Query query; + private SelectTable join; + + QueryJoin(Query query, SelectTable join) { + this.query = query; + this.join = join; + } + + public QueryJoinCondition on(boolean x) { + query.getFrom().getAliasDefinition().checkMultipleBooleans(); + return addPrimitive(x); + } + + public QueryJoinCondition on(byte x) { + return addPrimitive(x); + } + + public QueryJoinCondition on(short x) { + return addPrimitive(x); + } + + public QueryJoinCondition on(int x) { + return addPrimitive(x); + } + + public QueryJoinCondition on(long x) { + return addPrimitive(x); + } + + public QueryJoinCondition on(float x) { + return addPrimitive(x); + } + + public QueryJoinCondition on(double x) { + return addPrimitive(x); + } + + private QueryJoinCondition addPrimitive(A x) { + A alias = query.getPrimitiveAliasByValue(x); + if (alias == null) { + // this will result in an unmapped field exception + return new QueryJoinCondition(query, join, x); + } + return new QueryJoinCondition(query, join, alias); + } + + public QueryJoinCondition on(A x) { + return new QueryJoinCondition(query, join, x); + } +} diff --git a/src/main/java/com/iciql/QueryJoinCondition.java b/src/main/java/com/iciql/QueryJoinCondition.java new file mode 100644 index 0000000..6dfd218 --- /dev/null +++ b/src/main/java/com/iciql/QueryJoinCondition.java @@ -0,0 +1,83 @@ +/* + * 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; + + +/** + * This class represents a query with join and an incomplete condition. + * + * @param + * the incomplete condition data type + */ + +public class QueryJoinCondition { + + private Query query; + private SelectTable join; + private A x; + + QueryJoinCondition(Query query, SelectTable join, A x) { + this.query = query; + this.join = join; + this.x = x; + } + + public Query is(boolean y) { + return addPrimitive(y); + } + + public Query is(byte y) { + return addPrimitive(y); + } + + public Query is(short y) { + return addPrimitive(y); + } + + public Query is(int y) { + return addPrimitive(y); + } + + public Query is(long y) { + return addPrimitive(y); + } + + public Query is(float y) { + return addPrimitive(y); + } + + public Query is(double y) { + return addPrimitive(y); + } + + @SuppressWarnings("unchecked") + private Query addPrimitive(Object o) { + A alias = query.getPrimitiveAliasByValue((A) o); + if (alias == null) { + join.addConditionToken(new Condition(x, (A) o, CompareType.EQUAL)); + } else { + join.addConditionToken(new Condition(x, alias, CompareType.EQUAL)); + } + return query; + } + + public Query is(A y) { + join.addConditionToken(new Condition(x, y, CompareType.EQUAL)); + return query; + } +} diff --git a/src/main/java/com/iciql/QueryWhere.java b/src/main/java/com/iciql/QueryWhere.java new file mode 100644 index 0000000..5baa5ab --- /dev/null +++ b/src/main/java/com/iciql/QueryWhere.java @@ -0,0 +1,501 @@ +/* + * 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.util.List; + +/** + * This class represents a query with a condition. + * + * @param + * the return type + */ + +public class QueryWhere { + + Query query; + + QueryWhere(Query query) { + this.query = query; + } + + /** + * Specify an AND condition with a mapped primitive boolean. + * + * @param x + * the primitive boolean field to query + * @return a query condition to continue building the condition + */ + public QueryCondition and(boolean x) { + query.getFrom().getAliasDefinition().checkMultipleBooleans(); + return addPrimitive(ConditionAndOr.AND, x); + } + + /** + * Specify an AND condition with a mapped primitive byte. + * + * @param x + * the primitive byte field to query + * @return a query condition to continue building the condition + */ + public QueryCondition and(byte x) { + return addPrimitive(ConditionAndOr.AND, x); + } + + /** + * Specify an AND condition with a mapped primitive short. + * + * @param x + * the primitive short field to query + * @return a query condition to continue building the condition + */ + public QueryCondition and(short x) { + return addPrimitive(ConditionAndOr.AND, x); + } + + /** + * Specify an AND condition with a mapped primitive int. + * + * @param x + * the primitive int field to query + * @return a query condition to continue building the condition + */ + public QueryCondition and(int x) { + return addPrimitive(ConditionAndOr.AND, x); + } + + /** + * Specify an AND condition with a mapped primitive long. + * + * @param x + * the primitive long field to query + * @return a query condition to continue building the condition + */ + public QueryCondition and(long x) { + return addPrimitive(ConditionAndOr.AND, x); + } + + /** + * Specify an AND condition with a mapped primitive float. + * + * @param x + * the primitive float field to query + * @return a query condition to continue building the condition + */ + public QueryCondition and(float x) { + return addPrimitive(ConditionAndOr.AND, x); + } + + /** + * Specify an AND condition with a mapped primitive double. + * + * @param x + * the primitive double field to query + * @return a query condition to continue building the condition + */ + public QueryCondition and(double x) { + return addPrimitive(ConditionAndOr.AND, x); + } + + private QueryCondition addPrimitive(ConditionAndOr condition, A x) { + query.addConditionToken(condition); + A alias = query.getPrimitiveAliasByValue(x); + if (alias == null) { + // this will result in an unmapped field exception + return new QueryCondition(query, x); + } + return new QueryCondition(query, alias); + } + + /** + * Specify an AND condition with a mapped Object field. + * + * @param x + * the Object field to query + * @return a query condition to continue building the condition + */ + public QueryCondition and(A x) { + query.getFrom().getAliasDefinition().checkMultipleEnums(x); + query.addConditionToken(ConditionAndOr.AND); + return new QueryCondition(query, x); + } + + /** + * Specify an OR condition with a mapped primitive boolean. + * + * @param x + * the primitive boolean field to query + * @return a query condition to continue building the condition + */ + public QueryCondition or(boolean x) { + query.getFrom().getAliasDefinition().checkMultipleBooleans(); + return addPrimitive(ConditionAndOr.OR, x); + } + + /** + * Specify an OR condition with a mapped primitive byte. + * + * @param x + * the primitive byte field to query + * @return a query condition to continue building the condition + */ + public QueryCondition or(byte x) { + return addPrimitive(ConditionAndOr.OR, x); + } + + /** + * Specify an OR condition with a mapped primitive short. + * + * @param x + * the primitive short field to query + * @return a query condition to continue building the condition + */ + public QueryCondition or(short x) { + return addPrimitive(ConditionAndOr.OR, x); + } + + /** + * Specify an OR condition with a mapped primitive int. + * + * @param x + * the primitive int field to query + * @return a query condition to continue building the condition + */ + public QueryCondition or(int x) { + return addPrimitive(ConditionAndOr.OR, x); + } + + /** + * Specify an OR condition with a mapped primitive long. + * + * @param x + * the primitive long field to query + * @return a query condition to continue building the condition + */ + public QueryCondition or(long x) { + return addPrimitive(ConditionAndOr.OR, x); + } + + /** + * Specify an OR condition with a mapped primitive float. + * + * @param x + * the primitive float field to query + * @return a query condition to continue building the condition + */ + public QueryCondition or(float x) { + return addPrimitive(ConditionAndOr.OR, x); + } + + /** + * Specify an OR condition with a mapped primitive double. + * + * @param x + * the primitive double field to query + * @return a query condition to continue building the condition + */ + public QueryCondition or(double x) { + return addPrimitive(ConditionAndOr.OR, x); + } + + /** + * Specify an OR condition with a mapped Object field. + * + * @param x + * the Object field to query + * @return a query condition to continue building the condition + */ + public QueryCondition or(A x) { + query.getFrom().getAliasDefinition().checkMultipleEnums(x); + query.addConditionToken(ConditionAndOr.OR); + return new QueryCondition(query, x); + } + + public QueryWhere limit(long limit) { + query.limit(limit); + return this; + } + + public QueryWhere offset(long offset) { + query.offset(offset); + return this; + } + + public String getSQL() { + SQLStatement stat = new SQLStatement(query.getDb()); + stat.appendSQL("SELECT *"); + query.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 query.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 query.toSQL(distinct); + } + + /** + * 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) { + return query.toSQL(distinct, k); + } + + public SubQuery subQuery(Z x) { + return new SubQuery(query, x); + } + + public SubQuery subQuery(boolean x) { + return subQuery(query.getPrimitiveAliasByValue(x)); + } + + public SubQuery subQuery(byte x) { + return subQuery(query.getPrimitiveAliasByValue(x)); + } + + public SubQuery subQuery(short x) { + return subQuery(query.getPrimitiveAliasByValue(x)); + } + + public SubQuery subQuery(int x) { + return subQuery(query.getPrimitiveAliasByValue(x)); + } + + public SubQuery subQuery(long x) { + return subQuery(query.getPrimitiveAliasByValue(x)); + } + + public SubQuery subQuery(float x) { + return subQuery(query.getPrimitiveAliasByValue(x)); + } + + public SubQuery subQuery(double x) { + return subQuery(query.getPrimitiveAliasByValue(x)); + } + + public List select(Z x) { + return query.select(x); + } + + public List selectDistinct(Z x) { + return query.selectDistinct(x); + } + + public X selectFirst(Z x) { + List list = query.select(x); + return list.isEmpty() ? null : list.get(0); + } + + public List select() { + return query.select(); + } + + public T selectFirst() { + List list = select(); + return list.isEmpty() ? null : list.get(0); + } + + public List selectDistinct() { + return query.selectDistinct(); + } + + public void createView(Class viewClass) { + query.createView(viewClass); + } + + public void replaceView(Class viewClass) { + query.replaceView(viewClass); + } + + /** + * Order by primitive boolean field + * + * @param field + * a primitive boolean field + * @return the query + */ + public QueryWhere orderBy(boolean field) { + query.getFrom().getAliasDefinition().checkMultipleBooleans(); + return orderByPrimitive(field); + } + + /** + * Order by primitive byte field + * + * @param field + * a primitive byte field + * @return the query + */ + public QueryWhere orderBy(byte field) { + return orderByPrimitive(field); + } + + /** + * Order by primitive short field + * + * @param field + * a primitive short field + * @return the query + */ + public QueryWhere orderBy(short field) { + return orderByPrimitive(field); + } + + public QueryWhere orderBy(int field) { + return orderByPrimitive(field); + } + + /** + * Order by primitive long field + * + * @param field + * a primitive long field + * @return the query + */ + public QueryWhere orderBy(long field) { + return orderByPrimitive(field); + } + + /** + * Order by primitive float field + * + * @param field + * a primitive float field + * @return the query + */ + public QueryWhere orderBy(float field) { + return orderByPrimitive(field); + } + + /** + * Order by primitive double field + * + * @param field + * a primitive double field + * @return the query + */ + public QueryWhere orderBy(double field) { + return orderByPrimitive(field); + } + + private QueryWhere orderByPrimitive(Object field) { + query.orderByPrimitive(field); + return this; + } + + public QueryWhere orderBy(Object field) { + query.getFrom().getAliasDefinition().checkMultipleEnums(field); + query.orderBy(field); + return this; + } + + /** + * Order by a number of Object columns. + * + * @param expressions + * the order by expressions + * @return the query + */ + + public QueryWhere orderBy(Object... expressions) { + query.orderBy(expressions); + return this; + } + + public QueryWhere orderByNullsFirst(Object expr) { + query.getFrom().getAliasDefinition().checkMultipleEnums(expr); + OrderExpression e = new OrderExpression(query, expr, false, true, false); + query.addOrderBy(e); + return this; + } + + public QueryWhere orderByNullsLast(Object expr) { + query.getFrom().getAliasDefinition().checkMultipleEnums(expr); + OrderExpression e = new OrderExpression(query, expr, false, false, true); + query.addOrderBy(e); + return this; + } + + public QueryWhere orderByDesc(Object expr) { + query.getFrom().getAliasDefinition().checkMultipleEnums(expr); + OrderExpression e = new OrderExpression(query, expr, true, false, false); + query.addOrderBy(e); + return this; + } + + public QueryWhere orderByDescNullsFirst(Object expr) { + query.getFrom().getAliasDefinition().checkMultipleEnums(expr); + OrderExpression e = new OrderExpression(query, expr, true, true, false); + query.addOrderBy(e); + return this; + } + + public QueryWhere orderByDescNullsLast(Object expr) { + query.getFrom().getAliasDefinition().checkMultipleEnums(expr); + OrderExpression e = new OrderExpression(query, expr, true, false, true); + query.addOrderBy(e); + return this; + } + + public int delete() { + return query.delete(); + } + + public int update() { + return query.update(); + } + + public long selectCount() { + return query.selectCount(); + } + +} diff --git a/src/main/java/com/iciql/RuntimeParameter.java b/src/main/java/com/iciql/RuntimeParameter.java new file mode 100644 index 0000000..0fbedba --- /dev/null +++ b/src/main/java/com/iciql/RuntimeParameter.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012 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; + +/** + * A runtime parameter is used to generate x=? conditions so that iciql can + * build re-usable dynamic queries with parameter substitution done manually at + * runtime. + * + * @param + * the operand type + */ + +class RuntimeParameter implements Token { + + public final static String PARAMETER = ""; + + A x; + CompareType compareType; + + RuntimeParameter(A x, CompareType type) { + this.x = x; + this.compareType = type; + } + + public void appendSQL(SQLStatement stat, Query query) { + query.appendSQL(stat, null, x); + stat.appendSQL(" "); + stat.appendSQL(compareType.getString()); + if (compareType.hasRightExpression()) { + stat.appendSQL(" "); + query.appendSQL(stat, x, PARAMETER); + } + } +} diff --git a/src/main/java/com/iciql/RuntimeToken.java b/src/main/java/com/iciql/RuntimeToken.java new file mode 100644 index 0000000..cbfd882 --- /dev/null +++ b/src/main/java/com/iciql/RuntimeToken.java @@ -0,0 +1,57 @@ +/* + * 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.text.MessageFormat; + +import com.iciql.util.StringUtils; + +/** + * Represents a traditional PreparedStatment fragment like "id=?, name=?". + * + */ +public class RuntimeToken implements Token { + + final String fragment; + final Object[] args; + + public RuntimeToken(String fragment, Object... args) { + this.fragment = fragment; + this.args = args == null ? new Object[0] : args; + } + + /** + * Append the SQL to the given statement using the given query. + * + * @param stat + * the statement to append the SQL to + * @param query + * the query to use + */ + @Override + public void appendSQL(SQLStatement stat, Query query) { + int tokenCount = StringUtils.count('?', fragment); + if (tokenCount != args.length) { + throw new IciqlException(MessageFormat.format( + "Fragment \"{0}\" specifies {1} tokens but you supplied {2} args", fragment, tokenCount, + args.length)); + } + stat.appendSQL(fragment); + for (Object arg : args) { + stat.addParameter(arg); + } + } +} diff --git a/src/main/java/com/iciql/SQLDialect.java b/src/main/java/com/iciql/SQLDialect.java new file mode 100644 index 0000000..f62168e --- /dev/null +++ b/src/main/java/com/iciql/SQLDialect.java @@ -0,0 +1,206 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * Copyright 2012 Frédéric Gaillard. + * + * 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.sql.DatabaseMetaData; + +import com.iciql.TableDefinition.ConstraintForeignKeyDefinition; +import com.iciql.TableDefinition.ConstraintUniqueDefinition; +import com.iciql.TableDefinition.IndexDefinition; + +/** + * This interface defines points where iciql can build different statements + * depending on the database used. + */ +public interface SQLDialect { + + /** + * Configure the dialect from the database metadata. + * + * @param databaseName + * @param data + */ + void configureDialect(String databaseName, DatabaseMetaData data); + + /** + * Allows a dialect to substitute an SQL type. + * + * @param sqlType + * @return the dialect-safe type + */ + String convertSqlType(String sqlType); + + /** + * Returns a properly formatted table name for the dialect. + * + * @param schemaName + * the schema name, or null for no schema + * @param tableName + * the properly formatted table name + * @return the SQL snippet + */ + String prepareTableName(String schemaName, String tableName); + + /** + * Returns a properly formatted column name for the dialect. + * + * @param name + * the column name + * @return the properly formatted column name + */ + String prepareColumnName(String name); + + /** + * Get the CREATE TABLE statement. + * + * @param stat + * @param def + */ + void prepareCreateTable(SQLStatement stat, TableDefinition def); + + /** + * Get the DROP TABLE statement. + * + * @param stat + * @param def + */ + void prepareDropTable(SQLStatement stat, TableDefinition def); + + + /** + * Get the CREATE VIEW statement. + * + * @param stat + * return the SQL statement + * @param def + * table definition + */ + void prepareCreateView(SQLStatement stat, TableDefinition def); + + /** + * Get the CREATE VIEW statement. + * + * @param stat + * return the SQL statement + * @param def + * table definition + * @param fromWhere + */ + void prepareCreateView(SQLStatement stat, TableDefinition def, String fromWhere); + + /** + * Get the DROP VIEW statement. + * + * @param stat + * return the SQL statement + * @param def + * table definition + */ + void prepareDropView(SQLStatement stat, TableDefinition def); + + /** + * Get the CREATE INDEX statement. + * + * @param stat + * return the SQL statement + * @param schemaName + * the schema name + * @param tableName + * the table name + * @param index + * the index definition + */ + void prepareCreateIndex(SQLStatement stat, String schemaName, String tableName, IndexDefinition index); + + /** + * Get the ALTER statement. + * + * @param stat + * return the SQL statement + * @param schemaName + * the schema name + * @param tableName + * the table name + * @param constraint + * the constraint definition + */ + void prepareCreateConstraintForeignKey(SQLStatement stat, String schemaName, String tableName, ConstraintForeignKeyDefinition constraint); + + /** + * Get the ALTER statement. + * + * @param stat + * return the SQL statement + * @param schemaName + * the schema name + * @param tableName + * the table name + * @param constraint + * the constraint definition + * return the SQL statement + */ + void prepareCreateConstraintUnique(SQLStatement stat, String schemaName, String tableName, ConstraintUniqueDefinition constraint); + + /** + * Get a MERGE or REPLACE INTO statement. + * + * @param stat + * return the SQL statement + * @param schemaName + * the schema name + * @param tableName + * the table name + * @param def + * the table definition + * @param obj + * values + */ + void prepareMerge(SQLStatement stat, String schemaName, String tableName, TableDefinition def, + Object obj); + + /** + * Append "LIMIT limit OFFSET offset" to the SQL statement. + * + * @param stat + * the statement + * @param limit + * the limit + * @param offset + * the offset + */ + void appendLimitOffset(SQLStatement stat, long limit, long offset); + + /** + * Returns the preferred DATETIME class for the database. + *

+ * Either java.util.Date or java.sql.Timestamp + * + * @return preferred DATETIME class + */ + Class getDateTimeClass(); + + /** + * When building static string statements this method flattens an object to + * a string representation suitable for a static string statement. + * + * @param o + * @return the string equivalent of this object + */ + String prepareParameter(Object o); +} diff --git a/src/main/java/com/iciql/SQLDialectDefault.java b/src/main/java/com/iciql/SQLDialectDefault.java new file mode 100644 index 0000000..364db7b --- /dev/null +++ b/src/main/java/com/iciql/SQLDialectDefault.java @@ -0,0 +1,445 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * Copyright 2012 Frédéric Gaillard. + * + * 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.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.text.MessageFormat; +import java.text.SimpleDateFormat; + +import com.iciql.Iciql.ConstraintDeleteType; +import com.iciql.Iciql.ConstraintUpdateType; +import com.iciql.TableDefinition.ConstraintForeignKeyDefinition; +import com.iciql.TableDefinition.ConstraintUniqueDefinition; +import com.iciql.TableDefinition.FieldDefinition; +import com.iciql.TableDefinition.IndexDefinition; +import com.iciql.util.IciqlLogger; +import com.iciql.util.StatementBuilder; +import com.iciql.util.StringUtils; + +/** + * Default implementation of an SQL dialect. + */ +public class SQLDialectDefault implements SQLDialect { + + final String LITERAL = "'"; + + float databaseVersion; + String databaseName; + String productVersion; + + @Override + public String toString() { + return getClass().getName() + ": " + databaseName + " " + productVersion; + } + + @Override + public void configureDialect(String databaseName, DatabaseMetaData data) { + this.databaseName = databaseName; + try { + databaseVersion = Float.parseFloat(data.getDatabaseMajorVersion() + "." + + data.getDatabaseMinorVersion()); + productVersion = data.getDatabaseProductVersion(); + } catch (SQLException e) { + throw new IciqlException(e); + } + } + + /** + * Allows subclasses to change the type of a column for a CREATE statement. + * + * @param sqlType + * @return the SQL type or a preferred alternative + */ + @Override + public String convertSqlType(String sqlType) { + return sqlType; + } + + @Override + public Class getDateTimeClass() { + return java.util.Date.class; + } + + @Override + public String prepareTableName(String schemaName, String tableName) { + if (StringUtils.isNullOrEmpty(schemaName)) { + return tableName; + } + return schemaName + "." + tableName; + } + + @Override + public String prepareColumnName(String name) { + return name; + } + + @Override + public void prepareDropTable(SQLStatement stat, TableDefinition def) { + StatementBuilder buff = new StatementBuilder("DROP TABLE IF EXISTS " + + prepareTableName(def.schemaName, def.tableName)); + stat.setSQL(buff.toString()); + return; + } + + protected String prepareCreateTable(TableDefinition def) { + return "CREATE TABLE"; + } + + @Override + public void prepareCreateTable(SQLStatement stat, TableDefinition def) { + StatementBuilder buff = new StatementBuilder(); + buff.append(prepareCreateTable(def)); + buff.append(" "); + buff.append(prepareTableName(def.schemaName, def.tableName)).append('('); + + boolean hasIdentityColumn = false; + for (FieldDefinition field : def.fields) { + buff.appendExceptFirst(", "); + buff.append(prepareColumnName(field.columnName)).append(' '); + String dataType = field.dataType; + if (dataType.equals("VARCHAR")) { + // check to see if we should use VARCHAR or CLOB + if (field.length <= 0) { + dataType = "CLOB"; + } + buff.append(convertSqlType(dataType)); + if (field.length > 0) { + buff.append('(').append(field.length).append(')'); + } + } else if (dataType.equals("DECIMAL")) { + // DECIMAL(precision,scale) + buff.append(convertSqlType(dataType)); + if (field.length > 0) { + buff.append('(').append(field.length); + if (field.scale > 0) { + buff.append(',').append(field.scale); + } + buff.append(')'); + } + } else { + // other + hasIdentityColumn |= prepareColumnDefinition(buff, convertSqlType(dataType), + field.isAutoIncrement, field.isPrimaryKey); + } + + // default values + if (!field.isAutoIncrement && !field.isPrimaryKey) { + String dv = field.defaultValue; + if (!StringUtils.isNullOrEmpty(dv)) { + if (ModelUtils.isProperlyFormattedDefaultValue(dv) + && ModelUtils.isValidDefaultValue(field.field.getType(), dv)) { + buff.append(" DEFAULT " + dv); + } + } + } + + if (!field.nullable) { + buff.append(" NOT NULL"); + } + } + + // if table does not have identity column then specify primary key + if (!hasIdentityColumn) { + if (def.primaryKeyColumnNames != null && def.primaryKeyColumnNames.size() > 0) { + buff.append(", PRIMARY KEY("); + buff.resetCount(); + for (String n : def.primaryKeyColumnNames) { + buff.appendExceptFirst(", "); + buff.append(prepareColumnName(n)); + } + buff.append(')'); + } + } + buff.append(')'); + stat.setSQL(buff.toString()); + } + + @Override + public void prepareDropView(SQLStatement stat, TableDefinition def) { + StatementBuilder buff = new StatementBuilder("DROP VIEW " + + prepareTableName(def.schemaName, def.tableName)); + stat.setSQL(buff.toString()); + return; + } + + protected String prepareCreateView(TableDefinition def) { + return "CREATE VIEW"; + } + + @Override + public void prepareCreateView(SQLStatement stat, TableDefinition def) { + StatementBuilder buff = new StatementBuilder(); + buff.append(" FROM "); + buff.append(prepareTableName(def.schemaName, def.viewTableName)); + + StatementBuilder where = new StatementBuilder(); + for (FieldDefinition field : def.fields) { + if (!StringUtils.isNullOrEmpty(field.constraint)) { + where.appendExceptFirst(", "); + String col = prepareColumnName(field.columnName); + String constraint = field.constraint.replace("{0}", col).replace("this", col); + where.append(constraint); + } + } + if (where.length() > 0) { + buff.append(" WHERE "); + buff.append(where.toString()); + } + + prepareCreateView(stat, def, buff.toString()); + } + + @Override + public void prepareCreateView(SQLStatement stat, TableDefinition def, String fromWhere) { + StatementBuilder buff = new StatementBuilder(); + buff.append(prepareCreateView(def)); + buff.append(" "); + buff.append(prepareTableName(def.schemaName, def.tableName)); + + buff.append(" AS SELECT "); + for (FieldDefinition field : def.fields) { + buff.appendExceptFirst(", "); + buff.append(prepareColumnName(field.columnName)); + } + buff.append(fromWhere); + stat.setSQL(buff.toString()); + } + + protected boolean isIntegerType(String dataType) { + if ("INT".equals(dataType)) { + return true; + } else if ("BIGINT".equals(dataType)) { + return true; + } else if ("TINYINT".equals(dataType)) { + return true; + } else if ("SMALLINT".equals(dataType)) { + return true; + } + return false; + } + + protected boolean prepareColumnDefinition(StatementBuilder buff, String dataType, + boolean isAutoIncrement, boolean isPrimaryKey) { + buff.append(dataType); + if (isAutoIncrement) { + buff.append(" AUTO_INCREMENT"); + } + return false; + } + + @Override + public void prepareCreateIndex(SQLStatement stat, String schemaName, String tableName, + IndexDefinition index) { + StatementBuilder buff = new StatementBuilder(); + buff.append("CREATE "); + switch (index.type) { + case UNIQUE: + buff.append("UNIQUE "); + break; + case UNIQUE_HASH: + buff.append("UNIQUE "); + break; + default: + IciqlLogger.warn("{0} does not support hash indexes", getClass().getSimpleName()); + } + buff.append("INDEX "); + buff.append(index.indexName); + buff.append(" ON "); + // FIXME maybe we can use schemaName ? + // buff.append(prepareTableName(schemaName, tableName)); + buff.append(tableName); + buff.append("("); + for (String col : index.columnNames) { + buff.appendExceptFirst(", "); + buff.append(prepareColumnName(col)); + } + buff.append(") "); + + stat.setSQL(buff.toString().trim()); + } + + /** + * PostgreSQL and Derby do not support the SQL2003 MERGE syntax, but we can + * use a trick to insert a row if it does not exist and call update() in + * Db.merge() if the affected row count is 0. + *

+ * Databases that do support a MERGE syntax should override this method. + *

+ * http://stackoverflow.com/questions/407688 + */ + @Override + public void prepareMerge(SQLStatement stat, String schemaName, String tableName, + TableDefinition def, Object obj) { + StatementBuilder buff = new StatementBuilder("INSERT INTO "); + buff.append(prepareTableName(schemaName, tableName)); + buff.append(" ("); + buff.resetCount(); + for (FieldDefinition field : def.fields) { + buff.appendExceptFirst(", "); + buff.append(prepareColumnName(field.columnName)); + } + buff.append(") (SELECT "); + buff.resetCount(); + for (FieldDefinition field : def.fields) { + buff.appendExceptFirst(", "); + buff.append('?'); + Object value = def.getValue(obj, field); + stat.addParameter(value); + } + buff.append(" FROM "); + buff.append(prepareTableName(schemaName, tableName)); + buff.append(" WHERE "); + buff.resetCount(); + for (FieldDefinition field : def.fields) { + if (field.isPrimaryKey) { + buff.appendExceptFirst(" AND "); + buff.append(MessageFormat.format("{0} = ?", prepareColumnName(field.columnName))); + Object value = def.getValue(obj, field); + stat.addParameter(value); + } + } + buff.append(" HAVING count(*)=0)"); + stat.setSQL(buff.toString()); + } + + @Override + public void appendLimitOffset(SQLStatement stat, long limit, long offset) { + if (limit > 0) { + stat.appendSQL(" LIMIT " + limit); + } + if (offset > 0) { + stat.appendSQL(" OFFSET " + offset); + } + } + + @Override + public String prepareParameter(Object o) { + if (o instanceof String) { + return LITERAL + o.toString().replace(LITERAL, "''") + LITERAL; + } else if (o instanceof Character) { + return LITERAL + o.toString() + LITERAL; + } else if (o instanceof java.sql.Time) { + return LITERAL + new SimpleDateFormat("HH:mm:ss").format(o) + LITERAL; + } else if (o instanceof java.sql.Date) { + return LITERAL + new SimpleDateFormat("yyyy-MM-dd").format(o) + LITERAL; + } else if (o instanceof java.util.Date) { + return LITERAL + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(o) + LITERAL; + } + return o.toString(); + } + + @SuppressWarnings("incomplete-switch") + @Override + public void prepareCreateConstraintForeignKey(SQLStatement stat, String schemaName, String tableName, ConstraintForeignKeyDefinition constraint) { + StatementBuilder buff = new StatementBuilder(); + buff.append("ALTER TABLE "); + buff.append(prepareTableName(schemaName, tableName)); + buff.append(" ADD CONSTRAINT "); + buff.append(constraint.constraintName); + buff.append(" FOREIGN KEY "); + buff.append(" ("); + for (String col : constraint.foreignColumns) { + buff.appendExceptFirst(", "); + buff.append(prepareColumnName(col)); + } + buff.append(") "); + buff.append(" REFERENCES "); + buff.append(constraint.referenceTable); + buff.append(" ("); + buff.resetCount(); + for (String col : constraint.referenceColumns) { + buff.appendExceptFirst(", "); + buff.append(prepareColumnName(col)); + } + buff.append(") "); + if (constraint.deleteType != ConstraintDeleteType.UNSET) { + buff.append(" ON DELETE "); + switch (constraint.deleteType) { + case CASCADE: + buff.append("CASCADE "); + break; + case RESTRICT: + buff.append("RESTRICT "); + break; + case SET_NULL: + buff.append("SET NULL "); + break; + case NO_ACTION: + buff.append("NO ACTION "); + break; + case SET_DEFAULT: + buff.append("SET DEFAULT "); + break; + } + } + if (constraint.updateType != ConstraintUpdateType.UNSET) { + buff.append(" ON UPDATE "); + switch (constraint.updateType) { + case CASCADE: + buff.append("CASCADE "); + break; + case RESTRICT: + buff.append("RESTRICT "); + break; + case SET_NULL: + buff.append("SET NULL "); + break; + case NO_ACTION: + buff.append("NO ACTION "); + break; + case SET_DEFAULT: + buff.append("SET DEFAULT "); + break; + } + } + switch (constraint.deferrabilityType) { + case DEFERRABLE_INITIALLY_DEFERRED: + buff.append("DEFERRABLE INITIALLY DEFERRED "); + break; + case DEFERRABLE_INITIALLY_IMMEDIATE: + buff.append("DEFERRABLE INITIALLY IMMEDIATE "); + break; + case NOT_DEFERRABLE: + buff.append("NOT DEFERRABLE "); + break; + case UNSET: + break; + } + stat.setSQL(buff.toString().trim()); + } + + @Override + public void prepareCreateConstraintUnique(SQLStatement stat, String schemaName, String tableName, ConstraintUniqueDefinition constraint) { + StatementBuilder buff = new StatementBuilder(); + buff.append("ALTER TABLE "); + buff.append(prepareTableName(schemaName, tableName)); + buff.append(" ADD CONSTRAINT "); + buff.append(constraint.constraintName); + buff.append(" UNIQUE "); + buff.append(" ("); + for (String col : constraint.uniqueColumns) { + buff.appendExceptFirst(", "); + buff.append(prepareColumnName(col)); + } + buff.append(") "); + stat.setSQL(buff.toString().trim()); + } + +} \ No newline at end of file diff --git a/src/main/java/com/iciql/SQLDialectDerby.java b/src/main/java/com/iciql/SQLDialectDerby.java new file mode 100644 index 0000000..f954a7c --- /dev/null +++ b/src/main/java/com/iciql/SQLDialectDerby.java @@ -0,0 +1,71 @@ +/* + * 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 com.iciql.util.StatementBuilder; + +/** + * Derby database dialect. + */ +public class SQLDialectDerby extends SQLDialectDefault { + + @Override + public Class getDateTimeClass() { + return java.sql.Timestamp.class; + } + + @Override + public String convertSqlType(String sqlType) { + if ("TINYINT".equals(sqlType)) { + // Derby does not have a TINYINT/BYTE type + return "SMALLINT"; + } + return sqlType; + } + + @Override + public void appendLimitOffset(SQLStatement stat, long limit, long offset) { + // FETCH/OFFSET added in 10.5 + if (databaseVersion >= 10.5f) { + if (offset > 0) { + stat.appendSQL(" OFFSET " + offset + (offset == 1 ? " ROW" : " ROWS")); + } + if (limit > 0) { + stat.appendSQL(" FETCH NEXT " + limit + (limit == 1 ? " ROW" : " ROWS") + " ONLY"); + } + } + } + + @Override + protected boolean prepareColumnDefinition(StatementBuilder buff, String dataType, + boolean isAutoIncrement, boolean isPrimaryKey) { + String convertedType = convertSqlType(dataType); + buff.append(convertedType); + if (isIntegerType(dataType) && isAutoIncrement) { + buff.append(" GENERATED BY DEFAULT AS IDENTITY"); + } + return false; + } + + @Override + public void prepareDropTable(SQLStatement stat, TableDefinition def) { + StatementBuilder buff = new StatementBuilder("DROP TABLE " + + prepareTableName(def.schemaName, def.tableName)); + stat.setSQL(buff.toString()); + return; + } +} \ No newline at end of file diff --git a/src/main/java/com/iciql/SQLDialectH2.java b/src/main/java/com/iciql/SQLDialectH2.java new file mode 100644 index 0000000..6b3bab1 --- /dev/null +++ b/src/main/java/com/iciql/SQLDialectH2.java @@ -0,0 +1,135 @@ +/* + * 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 com.iciql.TableDefinition.FieldDefinition; +import com.iciql.TableDefinition.IndexDefinition; +import com.iciql.util.StatementBuilder; + +/** + * H2 database dialect. + */ +public class SQLDialectH2 extends SQLDialectDefault { + + /** + * CACHED tables are created by default. MEMORY tables are created upon + * request. + */ + @Override + protected String prepareCreateTable(TableDefinition def) { + if (def.memoryTable) { + return "CREATE MEMORY TABLE IF NOT EXISTS"; + } else { + return "CREATE CACHED TABLE IF NOT EXISTS"; + } + } + + @Override + protected String prepareCreateView(TableDefinition def) { + return "CREATE VIEW IF NOT EXISTS"; + } + + @Override + public void prepareDropView(SQLStatement stat, TableDefinition def) { + StatementBuilder buff = new StatementBuilder("DROP VIEW IF EXISTS " + + prepareTableName(def.schemaName, def.tableName)); + stat.setSQL(buff.toString()); + return; + } + + @Override + protected boolean prepareColumnDefinition(StatementBuilder buff, String dataType, + boolean isAutoIncrement, boolean isPrimaryKey) { + String convertedType = convertSqlType(dataType); + boolean isIdentity = false; + if (isIntegerType(dataType)) { + if (isAutoIncrement && isPrimaryKey) { + buff.append("IDENTITY"); + isIdentity = true; + } else if (isAutoIncrement) { + buff.append(convertedType); + buff.append(" AUTO_INCREMENT"); + } else { + buff.append(convertedType); + } + } else { + buff.append(convertedType); + } + return isIdentity; + } + + @Override + public void prepareCreateIndex(SQLStatement stat, String schema, String table, IndexDefinition index) { + StatementBuilder buff = new StatementBuilder(); + buff.append("CREATE "); + switch (index.type) { + case STANDARD: + break; + case UNIQUE: + buff.append("UNIQUE "); + break; + case HASH: + buff.append("HASH "); + break; + case UNIQUE_HASH: + buff.append("UNIQUE HASH "); + break; + } + buff.append("INDEX IF NOT EXISTS "); + buff.append(index.indexName); + buff.append(" ON "); + buff.append(table); + buff.append("("); + for (String col : index.columnNames) { + buff.appendExceptFirst(", "); + buff.append(col); + } + buff.append(")"); + stat.setSQL(buff.toString()); + } + + @Override + public void prepareMerge(SQLStatement stat, String schemaName, String tableName, + TableDefinition def, Object obj) { + StatementBuilder buff = new StatementBuilder("MERGE INTO "); + buff.append(prepareTableName(schemaName, tableName)).append(" ("); + buff.resetCount(); + for (FieldDefinition field : def.fields) { + buff.appendExceptFirst(", "); + buff.append(field.columnName); + } + buff.append(") KEY("); + buff.resetCount(); + for (FieldDefinition field : def.fields) { + if (field.isPrimaryKey) { + buff.appendExceptFirst(", "); + buff.append(field.columnName); + } + } + buff.append(") "); + buff.resetCount(); + buff.append("VALUES ("); + for (FieldDefinition field : def.fields) { + buff.appendExceptFirst(", "); + buff.append('?'); + Object value = def.getValue(obj, field); + stat.addParameter(value); + } + buff.append(')'); + stat.setSQL(buff.toString()); + } +} \ No newline at end of file diff --git a/src/main/java/com/iciql/SQLDialectHSQL.java b/src/main/java/com/iciql/SQLDialectHSQL.java new file mode 100644 index 0000000..82e6833 --- /dev/null +++ b/src/main/java/com/iciql/SQLDialectHSQL.java @@ -0,0 +1,149 @@ +/* + * 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.text.MessageFormat; + +import com.iciql.TableDefinition.FieldDefinition; +import com.iciql.util.StatementBuilder; + +/** + * HyperSQL database dialect. + */ +public class SQLDialectHSQL extends SQLDialectDefault { + + /** + * CACHED tables are created by default. MEMORY tables are created upon + * request. + */ + @Override + protected String prepareCreateTable(TableDefinition def) { + if (def.memoryTable) { + return "CREATE MEMORY TABLE IF NOT EXISTS"; + } else { + return "CREATE CACHED TABLE IF NOT EXISTS"; + } + } + + @Override + public void prepareDropView(SQLStatement stat, TableDefinition def) { + StatementBuilder buff = new StatementBuilder("DROP VIEW IF EXISTS " + + prepareTableName(def.schemaName, def.tableName)); + stat.setSQL(buff.toString()); + return; + } + + @Override + protected boolean prepareColumnDefinition(StatementBuilder buff, String dataType, + boolean isAutoIncrement, boolean isPrimaryKey) { + boolean isIdentity = false; + String convertedType = convertSqlType(dataType); + buff.append(convertedType); + if (isIntegerType(dataType) && isAutoIncrement && isPrimaryKey) { + buff.append(" IDENTITY"); + isIdentity = true; + } + return isIdentity; + } + + @Override + public void prepareMerge(SQLStatement stat, String schemaName, String tableName, + TableDefinition def, Object obj) { + final String valuePrefix = "v"; + StatementBuilder buff = new StatementBuilder("MERGE INTO "); + buff.append(prepareTableName(schemaName, tableName)); + // a, b, c.... + buff.append(" USING (VALUES("); + for (FieldDefinition field : def.fields) { + buff.appendExceptFirst(", "); + buff.append("CAST(? AS "); + String dataType = convertSqlType(field.dataType); + buff.append(dataType); + if ("VARCHAR".equals(dataType)) { + if (field.length > 0) { + // VARCHAR(x) + buff.append(MessageFormat.format("({0})", field.length)); + } + } else if ("DECIMAL".equals(dataType)) { + if (field.length > 0) { + if (field.scale > 0) { + // DECIMAL(x,y) + buff.append(MessageFormat.format("({0},{1})", field.length, field.scale)); + } else { + // DECIMAL(x) + buff.append(MessageFormat.format("({0})", field.length)); + } + } + } + buff.append(')'); + Object value = def.getValue(obj, field); + stat.addParameter(value); + } + + // map to temporary table + buff.resetCount(); + buff.append(")) AS vals ("); + for (FieldDefinition field : def.fields) { + buff.appendExceptFirst(", "); + buff.append(prepareColumnName(valuePrefix + field.columnName)); + } + + buff.append(") ON "); + + // create the ON condition + // (va, vb) = (va,vb) + String[] prefixes = { "", valuePrefix }; + for (int i = 0; i < prefixes.length; i++) { + String prefix = prefixes[i]; + buff.resetCount(); + buff.append('('); + for (FieldDefinition field : def.fields) { + if (field.isPrimaryKey) { + buff.appendExceptFirst(", "); + buff.append(prepareColumnName(prefix + field.columnName)); + } + } + buff.append(")"); + if (i == 0) { + buff.append('='); + } + } + + // UPDATE + // set a=va + buff.append(" WHEN MATCHED THEN UPDATE SET "); + buff.resetCount(); + for (FieldDefinition field : def.fields) { + buff.appendExceptFirst(", "); + buff.append(prepareColumnName(field.columnName)); + buff.append('='); + buff.append(prepareColumnName(valuePrefix + field.columnName)); + } + + // INSERT + // insert va, vb, vc.... + buff.append(" WHEN NOT MATCHED THEN INSERT "); + buff.resetCount(); + buff.append(" VALUES ("); + for (FieldDefinition field : def.fields) { + buff.appendExceptFirst(", "); + buff.append(prepareColumnName(valuePrefix + field.columnName)); + } + buff.append(')'); + stat.setSQL(buff.toString()); + } +} \ No newline at end of file diff --git a/src/main/java/com/iciql/SQLDialectMSSQL.java b/src/main/java/com/iciql/SQLDialectMSSQL.java new file mode 100644 index 0000000..92b1297 --- /dev/null +++ b/src/main/java/com/iciql/SQLDialectMSSQL.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012 Alex Telepov. + * Copyright 2012 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; + +/** + * MS SQL Server database dialect. + */ +public class SQLDialectMSSQL extends SQLDialectDefault { + + /** + * Append limit and offset rows + * + * @param stat Statement + * @param limit Limit rows + * @param offset Offset rows + */ + @Override + public void appendLimitOffset(SQLStatement stat, long limit, long offset) { + if (offset > 0) { + throw new IciqlException("iciql does not support offset for MSSQL dialect!"); + } + StringBuilder query = new StringBuilder(stat.getSQL()); + + // for databaseVersion >= 2012 need Offset + if (limit > 0) { + int indexSelect = query.indexOf("SELECT"); + + if (indexSelect >= 0) { + StringBuilder subPathQuery = new StringBuilder(" TOP "); + subPathQuery.append(Long.toString(limit)); + + query.insert(indexSelect + "SELECT".length(), subPathQuery); + + stat.setSQL(query.toString()); + } + } + } +} diff --git a/src/main/java/com/iciql/SQLDialectMySQL.java b/src/main/java/com/iciql/SQLDialectMySQL.java new file mode 100644 index 0000000..52676d4 --- /dev/null +++ b/src/main/java/com/iciql/SQLDialectMySQL.java @@ -0,0 +1,93 @@ +/* + * 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 com.iciql.TableDefinition.FieldDefinition; +import com.iciql.util.StatementBuilder; + +/** + * MySQL database dialect. + */ +public class SQLDialectMySQL extends SQLDialectDefault { + + @Override + public String convertSqlType(String sqlType) { + if (sqlType.equals("CLOB")) { + return "TEXT"; + } + return sqlType; + } + + @Override + protected String prepareCreateTable(TableDefinition def) { + return "CREATE TABLE IF NOT EXISTS"; + } + + @Override + public void prepareDropView(SQLStatement stat, TableDefinition def) { + StatementBuilder buff = new StatementBuilder("DROP VIEW IF EXISTS " + + prepareTableName(def.schemaName, def.tableName)); + stat.setSQL(buff.toString()); + return; + } + + @Override + public String prepareColumnName(String name) { + return "`" + name + "`"; + } + + @Override + protected boolean prepareColumnDefinition(StatementBuilder buff, String dataType, boolean isAutoIncrement, + boolean isPrimaryKey) { + String convertedType = convertSqlType(dataType); + buff.append(convertedType); + if (isIntegerType(dataType) && isAutoIncrement) { + buff.append(" AUTO_INCREMENT"); + } + return false; + } + + @Override + public void prepareMerge(SQLStatement stat, String schemaName, String tableName, + TableDefinition def, Object obj) { + StatementBuilder buff = new StatementBuilder("INSERT INTO "); + buff.append(prepareTableName(schemaName, tableName)).append(" ("); + buff.resetCount(); + for (FieldDefinition field : def.fields) { + buff.appendExceptFirst(", "); + buff.append(field.columnName); + } + buff.resetCount(); + buff.append(") VALUES ("); + for (FieldDefinition field : def.fields) { + buff.appendExceptFirst(", "); + buff.append('?'); + Object value = def.getValue(obj, field); + stat.addParameter(value); + } + buff.append(") ON DUPLICATE KEY UPDATE "); + buff.resetCount(); + for (FieldDefinition field : def.fields) { + buff.appendExceptFirst(", "); + buff.append(field.columnName); + buff.append("=VALUES("); + buff.append(field.columnName); + buff.append(')'); + } + stat.setSQL(buff.toString()); + } +} \ No newline at end of file diff --git a/src/main/java/com/iciql/SQLDialectPostgreSQL.java b/src/main/java/com/iciql/SQLDialectPostgreSQL.java new file mode 100644 index 0000000..fc115ab --- /dev/null +++ b/src/main/java/com/iciql/SQLDialectPostgreSQL.java @@ -0,0 +1,103 @@ +/* + * 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 com.iciql.TableDefinition.IndexDefinition; +import com.iciql.util.StatementBuilder; + +/** + * PostgreSQL database dialect. + */ +public class SQLDialectPostgreSQL extends SQLDialectDefault { + + @Override + public Class getDateTimeClass() { + return java.sql.Timestamp.class; + } + + @Override + public String convertSqlType(String sqlType) { + if ("DOUBLE".equals(sqlType)) { + return "DOUBLE PRECISION"; + } else if ("TINYINT".equals(sqlType)) { + // PostgreSQL does not have a byte type + return "SMALLINT"; + } else if ("CLOB".equals(sqlType)) { + return "TEXT"; + } else if ("BLOB".equals(sqlType)) { + return "BYTEA"; + } + return sqlType; + } + + @Override + protected boolean prepareColumnDefinition(StatementBuilder buff, String dataType, + boolean isAutoIncrement, boolean isPrimaryKey) { + String convertedType = convertSqlType(dataType); + if (isIntegerType(dataType)) { + if (isAutoIncrement) { + if ("BIGINT".equals(dataType)) { + buff.append("BIGSERIAL"); + } else { + buff.append("SERIAL"); + } + } else { + buff.append(convertedType); + } + } else { + buff.append(convertedType); + } + return false; + } + + @Override + public void prepareCreateIndex(SQLStatement stat, String schemaName, String tableName, + IndexDefinition index) { + StatementBuilder buff = new StatementBuilder(); + buff.append("CREATE "); + switch (index.type) { + case UNIQUE: + buff.append("UNIQUE "); + break; + case UNIQUE_HASH: + buff.append("UNIQUE "); + break; + } + buff.append("INDEX "); + buff.append(index.indexName); + buff.append(" ON "); + buff.append(tableName); + + switch (index.type) { + case HASH: + buff.append(" USING HASH"); + break; + case UNIQUE_HASH: + buff.append(" USING HASH"); + break; + } + + buff.append(" ("); + for (String col : index.columnNames) { + buff.appendExceptFirst(", "); + buff.append(prepareColumnName(col)); + } + buff.append(") "); + + stat.setSQL(buff.toString().trim()); + } +} \ No newline at end of file diff --git a/src/main/java/com/iciql/SQLStatement.java b/src/main/java/com/iciql/SQLStatement.java new file mode 100644 index 0000000..394fc42 --- /dev/null +++ b/src/main/java/com/iciql/SQLStatement.java @@ -0,0 +1,190 @@ +/* + * 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.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.StringTokenizer; + +import com.iciql.util.JdbcUtils; + +/** + * This class represents a parameterized SQL statement. + */ + +public class SQLStatement { + private Db db; + private StringBuilder buff = new StringBuilder(); + private String sql; + private ArrayList params = new ArrayList(); + + SQLStatement(Db db) { + this.db = db; + } + + public void setSQL(String sql) { + this.sql = sql; + buff = new StringBuilder(sql); + } + + public SQLStatement appendSQL(String s) { + buff.append(s); + sql = null; + return this; + } + + public SQLStatement appendTable(String schema, String table) { + return appendSQL(db.getDialect().prepareTableName(schema, table)); + } + + public SQLStatement appendColumn(String column) { + return appendSQL(db.getDialect().prepareColumnName(column)); + } + + /** + * getSQL returns a simple string representation of the parameterized + * statement which will be used later, internally, with prepareStatement. + * + * @return a simple sql statement + */ + String getSQL() { + if (sql == null) { + sql = buff.toString(); + } + return sql; + } + + /** + * toSQL creates a static sql statement with the referenced parameters + * encoded in the statement. + * + * @return a complete sql statement + */ + String toSQL() { + if (sql == null) { + sql = buff.toString(); + } + if (params.size() == 0) { + return sql; + } + StringBuilder sb = new StringBuilder(); + // TODO this needs to me more sophisticated + StringTokenizer st = new StringTokenizer(sql, "?", false); + int i = 0; + while (st.hasMoreTokens()) { + sb.append(st.nextToken()); + if (i < params.size()) { + Object o = params.get(i); + if (RuntimeParameter.PARAMETER == o) { + // dynamic parameter + sb.append('?'); + } else { + // static parameter + sb.append(db.getDialect().prepareParameter(o)); + } + i++; + } + } + return sb.toString(); + } + + public SQLStatement addParameter(Object o) { + // Automatically convert java.util.Date to java.sql.Timestamp + // if the dialect requires java.sql.Timestamp objects (e.g. Derby) + if (o != null && o.getClass().equals(java.util.Date.class) + && db.getDialect().getDateTimeClass().equals(java.sql.Timestamp.class)) { + o = new java.sql.Timestamp(((java.util.Date) o).getTime()); + } + params.add(o); + return this; + } + + void execute() { + PreparedStatement ps = null; + try { + ps = prepare(false); + ps.execute(); + } catch (SQLException e) { + throw IciqlException.fromSQL(getSQL(), e); + } finally { + JdbcUtils.closeSilently(ps); + } + } + + ResultSet executeQuery() { + try { + return prepare(false).executeQuery(); + } catch (SQLException e) { + throw IciqlException.fromSQL(getSQL(), e); + } + } + + int executeUpdate() { + PreparedStatement ps = null; + try { + ps = prepare(false); + return ps.executeUpdate(); + } catch (SQLException e) { + throw IciqlException.fromSQL(getSQL(), e); + } finally { + JdbcUtils.closeSilently(ps); + } + } + + long executeInsert() { + PreparedStatement ps = null; + try { + ps = prepare(true); + ps.executeUpdate(); + long identity = -1; + ResultSet rs = ps.getGeneratedKeys(); + if (rs != null && rs.next()) { + identity = rs.getLong(1); + } + JdbcUtils.closeSilently(rs); + return identity; + } catch (SQLException e) { + throw IciqlException.fromSQL(getSQL(), e); + } finally { + JdbcUtils.closeSilently(ps); + } + } + + private void setValue(PreparedStatement prep, int parameterIndex, Object x) { + try { + prep.setObject(parameterIndex, x); + } catch (SQLException e) { + IciqlException ix = new IciqlException(e, "error setting parameter {0} as {1}", parameterIndex, x + .getClass().getSimpleName()); + ix.setSQL(getSQL()); + throw ix; + } + } + + PreparedStatement prepare(boolean returnGeneratedKeys) { + PreparedStatement prep = db.prepare(getSQL(), returnGeneratedKeys); + for (int i = 0; i < params.size(); i++) { + Object o = params.get(i); + setValue(prep, i + 1, o); + } + return prep; + } + +} diff --git a/src/main/java/com/iciql/SelectColumn.java b/src/main/java/com/iciql/SelectColumn.java new file mode 100644 index 0000000..43a1a93 --- /dev/null +++ b/src/main/java/com/iciql/SelectColumn.java @@ -0,0 +1,57 @@ +/* + * 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 com.iciql.TableDefinition.FieldDefinition; + +/** + * This class represents a column of a table in a query. + * + * @param + * the table data type + */ + +class SelectColumn { + private SelectTable selectTable; + private FieldDefinition fieldDef; + + SelectColumn(SelectTable table, FieldDefinition fieldDef) { + this.selectTable = table; + this.fieldDef = fieldDef; + } + + void appendSQL(SQLStatement stat) { + if (selectTable.getQuery().isJoin()) { + stat.appendSQL(selectTable.getAs() + "." + fieldDef.columnName); + } else { + stat.appendColumn(fieldDef.columnName); + } + } + + FieldDefinition getFieldDefinition() { + return fieldDef; + } + + SelectTable getSelectTable() { + return selectTable; + } + + Object getCurrentValue() { + return fieldDef.getValue(selectTable.getCurrent()); + } +} diff --git a/src/main/java/com/iciql/SelectTable.java b/src/main/java/com/iciql/SelectTable.java new file mode 100644 index 0000000..37b42c4 --- /dev/null +++ b/src/main/java/com/iciql/SelectTable.java @@ -0,0 +1,112 @@ +/* + * 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.util.ArrayList; + +import com.iciql.util.Utils; + +/** + * This class represents a table in a query. + * + * @param + * the table class + */ + +class SelectTable { + + private Query query; + private Class clazz; + private T current; + private String as; + private TableDefinition aliasDef; + private boolean outerJoin; + private ArrayList joinConditions = Utils.newArrayList(); + private T alias; + + @SuppressWarnings("unchecked") + SelectTable(Db db, Query query, T alias, boolean outerJoin) { + this.alias = alias; + this.query = query; + this.outerJoin = outerJoin; + aliasDef = (TableDefinition) db.getTableDefinition(alias.getClass()); + clazz = Utils.getClass(alias); + as = "T" + Utils.nextAsCount(); + } + + T getAlias() { + return alias; + } + + T newObject() { + return Utils.newObject(clazz); + } + + TableDefinition getAliasDefinition() { + return aliasDef; + } + + void appendSQL(SQLStatement stat) { + if (query.isJoin()) { + stat.appendTable(aliasDef.schemaName, aliasDef.tableName).appendSQL(" AS " + as); + } else { + stat.appendTable(aliasDef.schemaName, aliasDef.tableName); + } + } + + void appendSQLAsJoin(SQLStatement stat, Query q) { + if (outerJoin) { + stat.appendSQL(" LEFT OUTER JOIN "); + } else { + stat.appendSQL(" INNER JOIN "); + } + appendSQL(stat); + if (!joinConditions.isEmpty()) { + stat.appendSQL(" ON "); + for (Token token : joinConditions) { + token.appendSQL(stat, q); + stat.appendSQL(" "); + } + } + } + + boolean getOuterJoin() { + return outerJoin; + } + + Query getQuery() { + return query; + } + + String getAs() { + return as; + } + + void addConditionToken(Token condition) { + joinConditions.add(condition); + } + + T getCurrent() { + return current; + } + + void setCurrent(T current) { + this.current = current; + } + +} diff --git a/src/main/java/com/iciql/SubQuery.java b/src/main/java/com/iciql/SubQuery.java new file mode 100644 index 0000000..398d214 --- /dev/null +++ b/src/main/java/com/iciql/SubQuery.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012 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; + +public class SubQuery { + + final Query query; + final Z z; + + public SubQuery(Query query, Z x) { + this.query = query; + this.z = x; + } + + public void appendSQL(SQLStatement stat) { + stat.appendSQL(query.toSubQuery(z)); + } +} diff --git a/src/main/java/com/iciql/SubQueryCondition.java b/src/main/java/com/iciql/SubQueryCondition.java new file mode 100644 index 0000000..effea3b --- /dev/null +++ b/src/main/java/com/iciql/SubQueryCondition.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012 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; + +/** + * A condition that contains a subquery. + * + * @param + * the operand type + */ + +class SubQueryCondition implements Token { + A x; + SubQuery subquery; + + SubQueryCondition(A x, SubQuery subquery) { + this.x = x; + this.subquery = subquery; + } + + public void appendSQL(SQLStatement stat, Query query) { + query.appendSQL(stat, null, x); + stat.appendSQL(" in ("); + subquery.appendSQL(stat); + stat.appendSQL(")"); + } +} diff --git a/src/main/java/com/iciql/TableDefinition.java b/src/main/java/com/iciql/TableDefinition.java new file mode 100644 index 0000000..6d8cb6e --- /dev/null +++ b/src/main/java/com/iciql/TableDefinition.java @@ -0,0 +1,1233 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * Copyright 2012 Frédéric Gaillard. + * + * 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.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.iciql.Iciql.ConstraintDeferrabilityType; +import com.iciql.Iciql.ConstraintDeleteType; +import com.iciql.Iciql.ConstraintUpdateType; +import com.iciql.Iciql.EnumId; +import com.iciql.Iciql.EnumType; +import com.iciql.Iciql.IQColumn; +import com.iciql.Iciql.IQConstraint; +import com.iciql.Iciql.IQContraintUnique; +import com.iciql.Iciql.IQContraintsUnique; +import com.iciql.Iciql.IQEnum; +import com.iciql.Iciql.IQContraintForeignKey; +import com.iciql.Iciql.IQContraintsForeignKey; +import com.iciql.Iciql.IQIgnore; +import com.iciql.Iciql.IQIndex; +import com.iciql.Iciql.IQIndexes; +import com.iciql.Iciql.IQSchema; +import com.iciql.Iciql.IQTable; +import com.iciql.Iciql.IQVersion; +import com.iciql.Iciql.IQView; +import com.iciql.Iciql.IndexType; +import com.iciql.util.IciqlLogger; +import com.iciql.util.StatementBuilder; +import com.iciql.util.StringUtils; +import com.iciql.util.Utils; + +/** + * A table definition contains the index definitions of a table, the field + * definitions, the table name, and other meta data. + * + * @param + * the table type + */ + +public class TableDefinition { + + /** + * The meta data of an index. + */ + + public static class IndexDefinition { + public IndexType type; + public String indexName; + + public List columnNames; + } + + /** + * The meta data of a constraint on foreign key. + */ + + public static class ConstraintForeignKeyDefinition { + + public String constraintName; + public List foreignColumns; + public String referenceTable; + public List referenceColumns; + public ConstraintDeleteType deleteType = ConstraintDeleteType.UNSET; + public ConstraintUpdateType updateType = ConstraintUpdateType.UNSET; + public ConstraintDeferrabilityType deferrabilityType = ConstraintDeferrabilityType.UNSET; + } + + /** + * The meta data of a unique constraint. + */ + + public static class ConstraintUniqueDefinition { + + public String constraintName; + public List uniqueColumns; + } + + + /** + * The meta data of a field. + */ + + static class FieldDefinition { + String columnName; + Field field; + String dataType; + int length; + int scale; + boolean isPrimaryKey; + boolean isAutoIncrement; + boolean trim; + boolean nullable; + String defaultValue; + EnumType enumType; + boolean isPrimitive; + String constraint; + + Object getValue(Object obj) { + try { + return field.get(obj); + } catch (Exception e) { + throw new IciqlException(e); + } + } + + private Object initWithNewObject(Object obj) { + Object o = Utils.newObject(field.getType()); + setValue(obj, o); + return o; + } + + private void setValue(Object obj, Object o) { + try { + if (!field.isAccessible()) { + field.setAccessible(true); + } + Class targetType = field.getType(); + if (targetType.isEnum()) { + o = Utils.convertEnum(o, targetType, enumType); + } else { + o = Utils.convert(o, targetType); + } + field.set(obj, o); + } catch (IciqlException e) { + throw e; + } catch (Exception e) { + throw new IciqlException(e); + } + } + + private Object read(ResultSet rs, int columnIndex) { + try { + return rs.getObject(columnIndex); + } catch (SQLException e) { + throw new IciqlException(e); + } + } + + @Override + public int hashCode() { + return columnName.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof FieldDefinition) { + return o.hashCode() == hashCode(); + } + return false; + } + } + + public ArrayList fields = Utils.newArrayList(); + String schemaName; + String tableName; + String viewTableName; + int tableVersion; + List primaryKeyColumnNames; + boolean memoryTable; + boolean multiplePrimitiveBools; + + private boolean createIfRequired = true; + private Class clazz; + private IdentityHashMap fieldMap = Utils.newIdentityHashMap(); + private ArrayList indexes = Utils.newArrayList(); + private ArrayList constraintsForeignKey = Utils.newArrayList(); + private ArrayList constraintsUnique = Utils.newArrayList(); + + TableDefinition(Class clazz) { + this.clazz = clazz; + schemaName = null; + tableName = clazz.getSimpleName(); + } + + Class getModelClass() { + return clazz; + } + + List getFields() { + return fields; + } + + void defineSchemaName(String schemaName) { + this.schemaName = schemaName; + } + + void defineTableName(String tableName) { + this.tableName = tableName; + } + + void defineViewTableName(String viewTableName) { + this.viewTableName = viewTableName; + } + + void defineMemoryTable() { + this.memoryTable = true; + } + + void defineSkipCreate() { + this.createIfRequired = false; + } + + /** + * Define a primary key by the specified model fields. + * + * @param modelFields + * the ordered list of model fields + */ + void definePrimaryKey(Object[] modelFields) { + List columnNames = mapColumnNames(modelFields); + setPrimaryKey(columnNames); + } + + /** + * Define a primary key by the specified column names. + * + * @param columnNames + * the ordered list of column names + */ + private void setPrimaryKey(List columnNames) { + primaryKeyColumnNames = Utils.newArrayList(columnNames); + List pkNames = Utils.newArrayList(); + for (String name : columnNames) { + pkNames.add(name.toLowerCase()); + } + // set isPrimaryKey flag for all field definitions + for (FieldDefinition fieldDefinition : fieldMap.values()) { + fieldDefinition.isPrimaryKey = pkNames.contains(fieldDefinition.columnName.toLowerCase()); + } + } + + private String getColumnName(A fieldObject) { + FieldDefinition def = fieldMap.get(fieldObject); + return def == null ? null : def.columnName; + } + + private ArrayList mapColumnNames(Object[] columns) { + ArrayList columnNames = Utils.newArrayList(); + for (Object column : columns) { + columnNames.add(getColumnName(column)); + } + return columnNames; + } + + /** + * Defines an index with the specified model fields. + * + * @param name + * the index name (optional) + * @param type + * the index type (STANDARD, HASH, UNIQUE, UNIQUE_HASH) + * @param modelFields + * the ordered list of model fields + */ + void defineIndex(String name, IndexType type, Object[] modelFields) { + List columnNames = mapColumnNames(modelFields); + addIndex(name, type, columnNames); + } + + /** + * Defines an index with the specified column names. + * + * @param type + * the index type (STANDARD, HASH, UNIQUE, UNIQUE_HASH) + * @param columnNames + * the ordered list of column names + */ + private void addIndex(String name, IndexType type, List columnNames) { + IndexDefinition index = new IndexDefinition(); + if (StringUtils.isNullOrEmpty(name)) { + index.indexName = tableName + "_idx_" + indexes.size(); + } else { + index.indexName = name; + } + index.columnNames = Utils.newArrayList(columnNames); + index.type = type; + indexes.add(index); + } + + /** + * Defines an unique constraint with the specified model fields. + * + * @param name + * the constraint name (optional) + * @param modelFields + * the ordered list of model fields + */ + void defineConstraintUnique(String name, Object[] modelFields) { + List columnNames = mapColumnNames(modelFields); + addConstraintUnique(name, columnNames); + } + + /** + * Defines an unique constraint. + * + * @param name + * @param columnNames + */ + private void addConstraintUnique(String name, List columnNames) { + ConstraintUniqueDefinition constraint = new ConstraintUniqueDefinition(); + if (StringUtils.isNullOrEmpty(name)) { + constraint.constraintName = tableName + "_unique_" + constraintsUnique.size(); + } else { + constraint.constraintName = name; + } + constraint.uniqueColumns = Utils.newArrayList(columnNames); + constraintsUnique.add(constraint); + } + + /** + * Defines a foreign key constraint with the specified model fields. + * + * @param name + * the constraint name (optional) + * @param modelFields + * the ordered list of model fields + */ + void defineForeignKey(String name, Object[] modelFields, String refTableName, Object[] refModelFields, + ConstraintDeleteType deleteType, ConstraintUpdateType updateType, + ConstraintDeferrabilityType deferrabilityType) { + List columnNames = mapColumnNames(modelFields); + List referenceColumnNames = mapColumnNames(refModelFields); + addConstraintForeignKey(name, columnNames, refTableName, referenceColumnNames, + deleteType, updateType, deferrabilityType); + } + + void defineColumnName(Object column, String columnName) { + FieldDefinition def = fieldMap.get(column); + if (def != null) { + def.columnName = columnName; + } + } + + void defineAutoIncrement(Object column) { + FieldDefinition def = fieldMap.get(column); + if (def != null) { + def.isAutoIncrement = true; + } + } + + void defineLength(Object column, int length) { + FieldDefinition def = fieldMap.get(column); + if (def != null) { + def.length = length; + } + } + + void defineScale(Object column, int scale) { + FieldDefinition def = fieldMap.get(column); + if (def != null) { + def.scale = scale; + } + } + + void defineTrim(Object column) { + FieldDefinition def = fieldMap.get(column); + if (def != null) { + def.trim = true; + } + } + + void defineNullable(Object column, boolean isNullable) { + FieldDefinition def = fieldMap.get(column); + if (def != null) { + def.nullable = isNullable; + } + } + + void defineDefaultValue(Object column, String defaultValue) { + FieldDefinition def = fieldMap.get(column); + if (def != null) { + def.defaultValue = defaultValue; + } + } + + void defineConstraint(Object column, String constraint) { + FieldDefinition def = fieldMap.get(column); + if (def != null) { + def.constraint = constraint; + } + } + + void mapFields() { + boolean byAnnotationsOnly = false; + boolean inheritColumns = false; + if (clazz.isAnnotationPresent(IQTable.class)) { + IQTable tableAnnotation = clazz.getAnnotation(IQTable.class); + byAnnotationsOnly = tableAnnotation.annotationsOnly(); + inheritColumns = tableAnnotation.inheritColumns(); + } + + if (clazz.isAnnotationPresent(IQView.class)) { + IQView viewAnnotation = clazz.getAnnotation(IQView.class); + byAnnotationsOnly = viewAnnotation.annotationsOnly(); + inheritColumns = viewAnnotation.inheritColumns(); + } + + List classFields = Utils.newArrayList(); + classFields.addAll(Arrays.asList(clazz.getDeclaredFields())); + if (inheritColumns) { + Class superClass = clazz.getSuperclass(); + classFields.addAll(Arrays.asList(superClass.getDeclaredFields())); + + if (superClass.isAnnotationPresent(IQView.class)) { + IQView superView = superClass.getAnnotation(IQView.class); + if (superView.inheritColumns()) { + // inherit columns from super.super.class + Class superSuperClass = superClass.getSuperclass(); + classFields.addAll(Arrays.asList(superSuperClass.getDeclaredFields())); + } + } else if (superClass.isAnnotationPresent(IQTable.class)) { + IQTable superTable = superClass.getAnnotation(IQTable.class); + if (superTable.inheritColumns()) { + // inherit columns from super.super.class + Class superSuperClass = superClass.getSuperclass(); + classFields.addAll(Arrays.asList(superSuperClass.getDeclaredFields())); + } + } + } + + Set uniqueFields = new LinkedHashSet(); + T defaultObject = Db.instance(clazz); + for (Field f : classFields) { + // check if we should skip this field + if (f.isAnnotationPresent(IQIgnore.class)) { + continue; + } + + // default to field name + String columnName = f.getName(); + boolean isAutoIncrement = false; + boolean isPrimaryKey = false; + int length = 0; + int scale = 0; + boolean trim = false; + boolean nullable = !f.getType().isPrimitive(); + EnumType enumType = null; + String defaultValue = ""; + String constraint = ""; + // configure Java -> SQL enum mapping + if (f.getType().isEnum()) { + enumType = EnumType.DEFAULT_TYPE; + if (f.getType().isAnnotationPresent(IQEnum.class)) { + // enum definition is annotated for all instances + IQEnum iqenum = f.getType().getAnnotation(IQEnum.class); + enumType = iqenum.value(); + } + if (f.isAnnotationPresent(IQEnum.class)) { + // this instance of the enum is annotated + IQEnum iqenum = f.getAnnotation(IQEnum.class); + enumType = iqenum.value(); + } + } + + // try using default object + try { + f.setAccessible(true); + Object value = f.get(defaultObject); + if (value != null) { + if (value.getClass().isEnum()) { + // enum default, convert to target type + Enum anEnum = (Enum) value; + Object o = Utils.convertEnum(anEnum, enumType); + defaultValue = ModelUtils.formatDefaultValue(o); + } else { + // object default + defaultValue = ModelUtils.formatDefaultValue(value); + } + } + } catch (IllegalAccessException e) { + throw new IciqlException(e, "failed to get default object for {0}", columnName); + } + + boolean hasAnnotation = f.isAnnotationPresent(IQColumn.class); + if (hasAnnotation) { + IQColumn col = f.getAnnotation(IQColumn.class); + if (!StringUtils.isNullOrEmpty(col.name())) { + columnName = col.name(); + } + isAutoIncrement = col.autoIncrement(); + isPrimaryKey = col.primaryKey(); + length = col.length(); + scale = col.scale(); + trim = col.trim(); + nullable = col.nullable(); + + // annotation overrides + if (!StringUtils.isNullOrEmpty(col.defaultValue())) { + defaultValue = col.defaultValue(); + } + } + + boolean hasConstraint = f.isAnnotationPresent(IQConstraint.class); + if (hasConstraint) { + IQConstraint con = f.getAnnotation(IQConstraint.class); + // annotation overrides + if (!StringUtils.isNullOrEmpty(con.value())) { + constraint = con.value(); + } + } + + boolean reflectiveMatch = !byAnnotationsOnly; + if (reflectiveMatch || hasAnnotation || hasConstraint) { + FieldDefinition fieldDef = new FieldDefinition(); + fieldDef.isPrimitive = f.getType().isPrimitive(); + fieldDef.field = f; + fieldDef.columnName = columnName; + fieldDef.isAutoIncrement = isAutoIncrement; + fieldDef.isPrimaryKey = isPrimaryKey; + fieldDef.length = length; + fieldDef.scale = scale; + fieldDef.trim = trim; + fieldDef.nullable = nullable; + fieldDef.defaultValue = defaultValue; + fieldDef.enumType = enumType; + fieldDef.dataType = ModelUtils.getDataType(fieldDef); + fieldDef.constraint = constraint; + uniqueFields.add(fieldDef); + } + } + fields.addAll(uniqueFields); + + List primaryKey = Utils.newArrayList(); + int primitiveBoolean = 0; + for (FieldDefinition fieldDef : fields) { + if (fieldDef.isPrimaryKey) { + primaryKey.add(fieldDef.columnName); + } + if (fieldDef.isPrimitive && fieldDef.field.getType().equals(boolean.class)) { + primitiveBoolean++; + } + } + if (primitiveBoolean > 1) { + multiplePrimitiveBools = true; + IciqlLogger + .warn("Model {0} has multiple primitive booleans! Possible where,set,join clause problem!"); + } + if (primaryKey.size() > 0) { + setPrimaryKey(primaryKey); + } + } + + void checkMultipleBooleans() { + if (multiplePrimitiveBools) { + throw new IciqlException( + "Can not explicitly reference a primitive boolean if there are multiple boolean fields in your model class!"); + } + } + + void checkMultipleEnums(Object o) { + if (o == null) { + return; + } + Class clazz = o.getClass(); + if (!clazz.isEnum()) { + return; + } + + int fieldCount = 0; + for (FieldDefinition fieldDef : fields) { + Class targetType = fieldDef.field.getType(); + if (clazz.equals(targetType)) { + fieldCount++; + } + } + + if (fieldCount > 1) { + throw new IciqlException( + "Can not explicitly reference {0} because there are {1} {0} fields in your model class!", + clazz.getSimpleName(), fieldCount); + } + } + + /** + * Optionally truncates strings to the maximum length and converts + * java.lang.Enum types to Strings or Integers. + */ + Object getValue(Object obj, FieldDefinition field) { + Object value = field.getValue(obj); + if (value == null) { + return value; + } + if (field.enumType != null) { + // convert enumeration to INT or STRING + Enum iqenum = (Enum) value; + switch (field.enumType) { + case NAME: + if (field.trim && field.length > 0) { + if (iqenum.name().length() > field.length) { + return iqenum.name().substring(0, field.length); + } + } + return iqenum.name(); + case ORDINAL: + return iqenum.ordinal(); + case ENUMID: + if (!EnumId.class.isAssignableFrom(value.getClass())) { + throw new IciqlException(field.field.getName() + " does not implement EnumId!"); + } + EnumId enumid = (EnumId) value; + return enumid.enumId(); + } + } + + if (field.trim && field.length > 0) { + if (value instanceof String) { + // clip strings + String s = (String) value; + if (s.length() > field.length) { + return s.substring(0, field.length); + } + return s; + } + return value; + } + + // return the value unchanged + return value; + } + + PreparedStatement createInsertStatement(Db db, Object obj, boolean returnKey) { + SQLStatement stat = new SQLStatement(db); + StatementBuilder buff = new StatementBuilder("INSERT INTO "); + buff.append(db.getDialect().prepareTableName(schemaName, tableName)).append('('); + for (FieldDefinition field : fields) { + if (skipInsertField(field, obj)) { + continue; + } + buff.appendExceptFirst(", "); + buff.append(db.getDialect().prepareColumnName(field.columnName)); + } + buff.append(") VALUES("); + buff.resetCount(); + for (FieldDefinition field : fields) { + if (skipInsertField(field, obj)) { + continue; + } + buff.appendExceptFirst(", "); + buff.append('?'); + Object value = getValue(obj, field); + if (value == null && !field.nullable) { + // try to interpret and instantiate a default value + value = ModelUtils.getDefaultValue(field, db.getDialect().getDateTimeClass()); + } + stat.addParameter(value); + } + buff.append(')'); + stat.setSQL(buff.toString()); + IciqlLogger.insert(stat.getSQL()); + return stat.prepare(returnKey); + } + + long insert(Db db, Object obj, boolean returnKey) { + if (!StringUtils.isNullOrEmpty(viewTableName)) { + throw new IciqlException("Iciql does not support inserting rows into views!"); + } + SQLStatement stat = new SQLStatement(db); + StatementBuilder buff = new StatementBuilder("INSERT INTO "); + buff.append(db.getDialect().prepareTableName(schemaName, tableName)).append('('); + for (FieldDefinition field : fields) { + if (skipInsertField(field, obj)) { + continue; + } + buff.appendExceptFirst(", "); + buff.append(db.getDialect().prepareColumnName(field.columnName)); + } + buff.append(") VALUES("); + buff.resetCount(); + for (FieldDefinition field : fields) { + if (skipInsertField(field, obj)) { + continue; + } + buff.appendExceptFirst(", "); + buff.append('?'); + Object value = getValue(obj, field); + if (value == null && !field.nullable) { + // try to interpret and instantiate a default value + value = ModelUtils.getDefaultValue(field, db.getDialect().getDateTimeClass()); + } + stat.addParameter(value); + } + buff.append(')'); + stat.setSQL(buff.toString()); + IciqlLogger.insert(stat.getSQL()); + if (returnKey) { + return stat.executeInsert(); + } + return stat.executeUpdate(); + } + + private boolean skipInsertField(FieldDefinition field, Object obj) { + if (field.isAutoIncrement) { + Object value = getValue(obj, field); + if (field.isPrimitive) { + // skip uninitialized primitive autoincrement values + if (value.toString().equals("0")) { + return true; + } + } else if (value == null) { + // skip null object autoincrement values + return true; + } + } else { + // conditionally skip insert of null + Object value = getValue(obj, field); + if (value == null) { + if (field.nullable) { + // skip null assignment, field is nullable + return true; + } else if (StringUtils.isNullOrEmpty(field.defaultValue)) { + IciqlLogger.warn("no default value, skipping null insert assignment for {0}.{1}", + tableName, field.columnName); + return true; + } + } + } + return false; + } + + int merge(Db db, Object obj) { + if (primaryKeyColumnNames == null || primaryKeyColumnNames.size() == 0) { + throw new IllegalStateException("No primary key columns defined for table " + obj.getClass() + + " - no update possible"); + } + SQLStatement stat = new SQLStatement(db); + db.getDialect().prepareMerge(stat, schemaName, tableName, this, obj); + IciqlLogger.merge(stat.getSQL()); + return stat.executeUpdate(); + } + + int update(Db db, Object obj) { + if (!StringUtils.isNullOrEmpty(viewTableName)) { + throw new IciqlException("Iciql does not support updating rows in views!"); + } + if (primaryKeyColumnNames == null || primaryKeyColumnNames.size() == 0) { + throw new IllegalStateException("No primary key columns defined for table " + obj.getClass() + + " - no update possible"); + } + SQLStatement stat = new SQLStatement(db); + StatementBuilder buff = new StatementBuilder("UPDATE "); + buff.append(db.getDialect().prepareTableName(schemaName, tableName)).append(" SET "); + buff.resetCount(); + + for (FieldDefinition field : fields) { + if (!field.isPrimaryKey) { + Object value = getValue(obj, field); + if (value == null && !field.nullable) { + // try to interpret and instantiate a default value + value = ModelUtils.getDefaultValue(field, db.getDialect().getDateTimeClass()); + } + buff.appendExceptFirst(", "); + buff.append(db.getDialect().prepareColumnName(field.columnName)); + buff.append(" = ?"); + stat.addParameter(value); + } + } + Object alias = Utils.newObject(obj.getClass()); + Query query = Query.from(db, alias); + boolean firstCondition = true; + for (FieldDefinition field : fields) { + if (field.isPrimaryKey) { + Object fieldAlias = field.getValue(alias); + Object value = field.getValue(obj); + if (field.isPrimitive) { + fieldAlias = query.getPrimitiveAliasByValue(fieldAlias); + } + if (!firstCondition) { + query.addConditionToken(ConditionAndOr.AND); + } + firstCondition = false; + query.addConditionToken(new Condition(fieldAlias, value, CompareType.EQUAL)); + } + } + stat.setSQL(buff.toString()); + query.appendWhere(stat); + IciqlLogger.update(stat.getSQL()); + return stat.executeUpdate(); + } + + int delete(Db db, Object obj) { + if (!StringUtils.isNullOrEmpty(viewTableName)) { + throw new IciqlException("Iciql does not support deleting rows from views!"); + } + if (primaryKeyColumnNames == null || primaryKeyColumnNames.size() == 0) { + throw new IllegalStateException("No primary key columns defined for table " + obj.getClass() + + " - no update possible"); + } + SQLStatement stat = new SQLStatement(db); + StatementBuilder buff = new StatementBuilder("DELETE FROM "); + buff.append(db.getDialect().prepareTableName(schemaName, tableName)); + buff.resetCount(); + Object alias = Utils.newObject(obj.getClass()); + Query query = Query.from(db, alias); + boolean firstCondition = true; + for (FieldDefinition field : fields) { + if (field.isPrimaryKey) { + Object fieldAlias = field.getValue(alias); + Object value = field.getValue(obj); + if (field.isPrimitive) { + fieldAlias = query.getPrimitiveAliasByValue(fieldAlias); + } + if (!firstCondition) { + query.addConditionToken(ConditionAndOr.AND); + } + firstCondition = false; + query.addConditionToken(new Condition(fieldAlias, value, CompareType.EQUAL)); + } + } + stat.setSQL(buff.toString()); + query.appendWhere(stat); + IciqlLogger.delete(stat.getSQL()); + return stat.executeUpdate(); + } + + TableDefinition createIfRequired(Db db) { + // globally enable/disable check of create if required + if (db.getSkipCreate()) { + return this; + } + if (!createIfRequired) { + // skip table and index creation + // but still check for upgrades + db.upgradeTable(this); + return this; + } + if (db.hasCreated(clazz)) { + return this; + } + SQLStatement stat = new SQLStatement(db); + if (StringUtils.isNullOrEmpty(viewTableName)) { + db.getDialect().prepareCreateTable(stat, this); + } else { + db.getDialect().prepareCreateView(stat, this); + } + IciqlLogger.create(stat.getSQL()); + try { + stat.executeUpdate(); + } catch (IciqlException e) { + if (e.getIciqlCode() != IciqlException.CODE_OBJECT_ALREADY_EXISTS) { + throw e; + } + } + + // create indexes + for (IndexDefinition index : indexes) { + stat = new SQLStatement(db); + db.getDialect().prepareCreateIndex(stat, schemaName, tableName, index); + IciqlLogger.create(stat.getSQL()); + try { + stat.executeUpdate(); + } catch (IciqlException e) { + if (e.getIciqlCode() != IciqlException.CODE_OBJECT_ALREADY_EXISTS + && e.getIciqlCode() != IciqlException.CODE_DUPLICATE_KEY) { + throw e; + } + } + } + + // create unique constraints + for (ConstraintUniqueDefinition constraint : constraintsUnique) { + stat = new SQLStatement(db); + db.getDialect().prepareCreateConstraintUnique(stat, schemaName, tableName, constraint); + IciqlLogger.create(stat.getSQL()); + try { + stat.executeUpdate(); + } catch (IciqlException e) { + if (e.getIciqlCode() != IciqlException.CODE_OBJECT_ALREADY_EXISTS + && e.getIciqlCode() != IciqlException.CODE_DUPLICATE_KEY) { + throw e; + } + } + } + + // create foreign keys constraints + for (ConstraintForeignKeyDefinition constraint : constraintsForeignKey) { + stat = new SQLStatement(db); + db.getDialect().prepareCreateConstraintForeignKey(stat, schemaName, tableName, constraint); + IciqlLogger.create(stat.getSQL()); + try { + stat.executeUpdate(); + } catch (IciqlException e) { + if (e.getIciqlCode() != IciqlException.CODE_OBJECT_ALREADY_EXISTS + && e.getIciqlCode() != IciqlException.CODE_DUPLICATE_KEY) { + throw e; + } + } + } + + // tables are created using IF NOT EXISTS + // but we may still need to upgrade + db.upgradeTable(this); + return this; + } + + void mapObject(Object obj) { + fieldMap.clear(); + initObject(obj, fieldMap); + + if (clazz.isAnnotationPresent(IQSchema.class)) { + IQSchema schemaAnnotation = clazz.getAnnotation(IQSchema.class); + // setup schema name mapping, if properly annotated + if (!StringUtils.isNullOrEmpty(schemaAnnotation.value())) { + schemaName = schemaAnnotation.value(); + } + } + + if (clazz.isAnnotationPresent(IQTable.class)) { + IQTable tableAnnotation = clazz.getAnnotation(IQTable.class); + + // setup table name mapping, if properly annotated + if (!StringUtils.isNullOrEmpty(tableAnnotation.name())) { + tableName = tableAnnotation.name(); + } + + // allow control over createTableIfRequired() + createIfRequired = tableAnnotation.create(); + + // model version + if (clazz.isAnnotationPresent(IQVersion.class)) { + IQVersion versionAnnotation = clazz.getAnnotation(IQVersion.class); + if (versionAnnotation.value() > 0) { + tableVersion = versionAnnotation.value(); + } + } + + // setup the primary index, if properly annotated + if (tableAnnotation.primaryKey().length > 0) { + List primaryKey = Utils.newArrayList(); + primaryKey.addAll(Arrays.asList(tableAnnotation.primaryKey())); + setPrimaryKey(primaryKey); + } + } + + if (clazz.isAnnotationPresent(IQView.class)) { + IQView viewAnnotation = clazz.getAnnotation(IQView.class); + + // setup view name mapping, if properly annotated + // set this as the table name so it fits in seemlessly with iciql + if (!StringUtils.isNullOrEmpty(viewAnnotation.name())) { + tableName = viewAnnotation.name(); + } else { + tableName = clazz.getSimpleName(); + } + + // setup source table name mapping, if properly annotated + if (!StringUtils.isNullOrEmpty(viewAnnotation.tableName())) { + viewTableName = viewAnnotation.tableName(); + } else { + // check for IQTable annotation on super class + Class superClass = clazz.getSuperclass(); + if (superClass.isAnnotationPresent(IQTable.class)) { + IQTable table = superClass.getAnnotation(IQTable.class); + if (StringUtils.isNullOrEmpty(table.name())) { + // super.SimpleClassName + viewTableName = superClass.getSimpleName(); + } else { + // super.IQTable.name() + viewTableName = table.name(); + } + } else if (superClass.isAnnotationPresent(IQView.class)) { + // super class is a view + IQView parentView = superClass.getAnnotation(IQView.class); + if (StringUtils.isNullOrEmpty(parentView.tableName())) { + // parent view does not define a tableName, must be inherited + Class superParent = superClass.getSuperclass(); + if (superParent != null && superParent.isAnnotationPresent(IQTable.class)) { + IQTable superParentTable = superParent.getAnnotation(IQTable.class); + if (StringUtils.isNullOrEmpty(superParentTable.name())) { + // super.super.SimpleClassName + viewTableName = superParent.getSimpleName(); + } else { + // super.super.IQTable.name() + viewTableName = superParentTable.name(); + } + } + } else { + // super.IQView.tableName() + viewTableName = parentView.tableName(); + } + } + + if (StringUtils.isNullOrEmpty(viewTableName)) { + // still missing view table name + throw new IciqlException("View model class \"{0}\" is missing a table name!", tableName); + } + } + + // allow control over createTableIfRequired() + createIfRequired = viewAnnotation.create(); + } + + if (clazz.isAnnotationPresent(IQIndex.class)) { + // single table index + IQIndex index = clazz.getAnnotation(IQIndex.class); + addIndex(index); + } + + if (clazz.isAnnotationPresent(IQIndexes.class)) { + // multiple table indexes + IQIndexes indexes = clazz.getAnnotation(IQIndexes.class); + for (IQIndex index : indexes.value()) { + addIndex(index); + } + } + + if (clazz.isAnnotationPresent(IQContraintUnique.class)) { + // single table unique constraint + IQContraintUnique constraint = clazz.getAnnotation(IQContraintUnique.class); + addConstraintUnique(constraint); + } + + if (clazz.isAnnotationPresent(IQContraintsUnique.class)) { + // multiple table unique constraints + IQContraintsUnique constraints = clazz.getAnnotation(IQContraintsUnique.class); + for (IQContraintUnique constraint : constraints.value()) { + addConstraintUnique(constraint); + } + } + + if (clazz.isAnnotationPresent(IQContraintForeignKey.class)) { + // single table constraint + IQContraintForeignKey constraint = clazz.getAnnotation(IQContraintForeignKey.class); + addConstraintForeignKey(constraint); + } + + if (clazz.isAnnotationPresent(IQContraintsForeignKey.class)) { + // multiple table constraints + IQContraintsForeignKey constraints = clazz.getAnnotation(IQContraintsForeignKey.class); + for (IQContraintForeignKey constraint : constraints.value()) { + addConstraintForeignKey(constraint); + } + } + + } + + private void addConstraintForeignKey(IQContraintForeignKey constraint) { + List foreignColumns = Arrays.asList(constraint.foreignColumns()); + List referenceColumns = Arrays.asList(constraint.referenceColumns()); + addConstraintForeignKey(constraint.name(), foreignColumns, constraint.referenceName(), referenceColumns, constraint.deleteType(), constraint.updateType(), constraint.deferrabilityType()); + } + + private void addConstraintUnique(IQContraintUnique constraint) { + List uniqueColumns = Arrays.asList(constraint.uniqueColumns()); + addConstraintUnique(constraint.name(), uniqueColumns); + } + + /** + * Defines a foreign key constraint with the specified parameters. + * + * @param name + * name of the constraint + * @param foreignColumns + * list of columns declared as foreign + * @param referenceName + * reference table name + * @param referenceColumns + * list of columns used in reference table + * @param deleteType + * action on delete + * @param updateType + * action on update + * @param deferrabilityType + * deferrability mode + */ + private void addConstraintForeignKey(String name, + List foreignColumns, String referenceName, + List referenceColumns, ConstraintDeleteType deleteType, + ConstraintUpdateType updateType, ConstraintDeferrabilityType deferrabilityType) { + ConstraintForeignKeyDefinition constraint = new ConstraintForeignKeyDefinition(); + if (StringUtils.isNullOrEmpty(name)) { + constraint.constraintName = tableName + "_fkey_" + constraintsForeignKey.size(); + } else { + constraint.constraintName = name; + } + constraint.foreignColumns = Utils.newArrayList(foreignColumns); + constraint.referenceColumns = Utils.newArrayList(referenceColumns); + constraint.referenceTable = referenceName; + constraint.deleteType = deleteType; + constraint.updateType = updateType; + constraint.deferrabilityType = deferrabilityType; + constraintsForeignKey.add(constraint); + } + + private void addIndex(IQIndex index) { + List columns = Arrays.asList(index.value()); + addIndex(index.name(), index.type(), columns); + } + + List getIndexes() { + return indexes; + } + + List getContraintsUnique() { + return constraintsUnique; + } + + List getContraintsForeignKey() { + return constraintsForeignKey; + } + + private void initObject(Object obj, Map map) { + for (FieldDefinition def : fields) { + Object newValue = def.initWithNewObject(obj); + map.put(newValue, def); + } + } + + void initSelectObject(SelectTable table, Object obj, Map> map) { + for (FieldDefinition def : fields) { + Object newValue = def.initWithNewObject(obj); + SelectColumn column = new SelectColumn(table, def); + map.put(newValue, column); + } + } + + /** + * Most queries executed by iciql have named select lists (select alpha, + * beta where...) but sometimes a wildcard select is executed (select *). + * When a wildcard query is executed on a table that has more columns than + * are mapped in your model object, this creates a column mapping issue. + * JaQu assumed that you can always use the integer index of the + * reflectively mapped field definition to determine position in the result + * set. + * + * This is not always true. + * + * iciql identifies when a select * query is executed and maps column names + * to a column index from the result set. If the select statement is + * explicit, then the standard assumed column index is used instead. + * + * @param rs + * @return + */ + int[] mapColumns(boolean wildcardSelect, ResultSet rs) { + int[] columns = new int[fields.size()]; + for (int i = 0; i < fields.size(); i++) { + try { + FieldDefinition def = fields.get(i); + int columnIndex; + if (wildcardSelect) { + // select * + // create column index by field name + columnIndex = rs.findColumn(def.columnName); + } else { + // select alpha, beta, gamma, etc + // explicit select order + columnIndex = i + 1; + } + columns[i] = columnIndex; + } catch (SQLException s) { + throw new IciqlException(s); + } + } + return columns; + } + + void readRow(Object item, ResultSet rs, int[] columns) { + for (int i = 0; i < fields.size(); i++) { + FieldDefinition def = fields.get(i); + int index = columns[i]; + Object o = def.read(rs, index); + def.setValue(item, o); + } + } + + void appendSelectList(SQLStatement stat) { + for (int i = 0; i < fields.size(); i++) { + if (i > 0) { + stat.appendSQL(", "); + } + FieldDefinition def = fields.get(i); + stat.appendColumn(def.columnName); + } + } + + void appendSelectList(SQLStatement stat, Query query, X x) { + // select t0.col1, t0.col2, t0.col3... + // select table1.col1, table1.col2, table1.col3... + String selectDot = ""; + SelectTable sel = query.getSelectTable(x); + if (sel != null) { + if (query.isJoin()) { + selectDot = sel.getAs() + "."; + } else { + String sn = sel.getAliasDefinition().schemaName; + String tn = sel.getAliasDefinition().tableName; + selectDot = query.getDb().getDialect().prepareTableName(sn, tn) + "."; + } + } + + for (int i = 0; i < fields.size(); i++) { + if (i > 0) { + stat.appendSQL(", "); + } + stat.appendSQL(selectDot); + FieldDefinition def = fields.get(i); + if (def.isPrimitive) { + Object obj = def.getValue(x); + Object alias = query.getPrimitiveAliasByValue(obj); + query.appendSQL(stat, x, alias); + } else { + Object obj = def.getValue(x); + query.appendSQL(stat, x, obj); + } + } + } +} diff --git a/src/main/java/com/iciql/TableInspector.java b/src/main/java/com/iciql/TableInspector.java new file mode 100644 index 0000000..b717203 --- /dev/null +++ b/src/main/java/com/iciql/TableInspector.java @@ -0,0 +1,723 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * Copyright 2012 Frédéric Gaillard. + * + * 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 static com.iciql.ValidationRemark.consider; +import static com.iciql.ValidationRemark.error; +import static com.iciql.ValidationRemark.warn; +import static com.iciql.util.JdbcUtils.closeSilently; +import static com.iciql.util.StringUtils.isNullOrEmpty; +import static java.text.MessageFormat.format; + +import java.io.Serializable; +import java.lang.reflect.Modifier; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.iciql.Iciql.IQColumn; +import com.iciql.Iciql.IQIndex; +import com.iciql.Iciql.IQIndexes; +import com.iciql.Iciql.IQSchema; +import com.iciql.Iciql.IQTable; +import com.iciql.Iciql.IndexType; +import com.iciql.TableDefinition.ConstraintForeignKeyDefinition; +import com.iciql.TableDefinition.ConstraintUniqueDefinition; +import com.iciql.TableDefinition.FieldDefinition; +import com.iciql.TableDefinition.IndexDefinition; +import com.iciql.util.StatementBuilder; +import com.iciql.util.StringUtils; +import com.iciql.util.Utils; + +/** + * Class to inspect the contents of a particular table including its indexes. + * This class does the bulk of the work in terms of model generation and model + * validation. + */ +public class TableInspector { + + private String schema; + private String table; + private Class dateTimeClass; + private List primaryKeys = Utils.newArrayList(); + private Map indexes; + private Map columns; + private final String eol = "\n"; + + TableInspector(String schema, String table, Class dateTimeClass) { + this.schema = schema; + this.table = table; + this.dateTimeClass = dateTimeClass; + } + + /** + * Tests to see if this TableInspector represents schema.table. + *

+ * + * @param schema + * the schema name + * @param table + * the table name + * @return true if the table matches + */ + boolean matches(String schema, String table) { + if (isNullOrEmpty(schema)) { + // table name matching + return this.table.equalsIgnoreCase(table); + } else if (isNullOrEmpty(table)) { + // schema name matching + return this.schema.equalsIgnoreCase(schema); + } else { + // exact table matching + return this.schema.equalsIgnoreCase(schema) && this.table.equalsIgnoreCase(table); + } + } + + /** + * Reads the DatabaseMetaData for the details of this table including + * primary keys and indexes. + * + * @param metaData + * the database meta data + */ + void read(DatabaseMetaData metaData) throws SQLException { + ResultSet rs = null; + + // primary keys + try { + rs = metaData.getPrimaryKeys(null, schema, table); + while (rs.next()) { + String c = rs.getString("COLUMN_NAME"); + primaryKeys.add(c); + } + closeSilently(rs); + + // indexes + rs = metaData.getIndexInfo(null, schema, table, false, true); + indexes = Utils.newHashMap(); + while (rs.next()) { + IndexInspector info = new IndexInspector(rs); + if (info.type.equals(IndexType.UNIQUE)) { + String name = info.name.toLowerCase(); + if (name.startsWith("primary") || name.startsWith("sys_idx_sys_pk") + || name.startsWith("sql") || name.endsWith("_pkey")) { + // skip primary key indexes + continue; + } + } + if (indexes.containsKey(info.name)) { + indexes.get(info.name).addColumn(rs); + } else { + indexes.put(info.name, info); + } + } + closeSilently(rs); + + // columns + rs = metaData.getColumns(null, schema, table, null); + columns = Utils.newHashMap(); + while (rs.next()) { + ColumnInspector col = new ColumnInspector(); + col.name = rs.getString("COLUMN_NAME"); + col.type = rs.getString("TYPE_NAME"); + col.clazz = ModelUtils.getClassForSqlType(col.type, dateTimeClass); + col.size = rs.getInt("COLUMN_SIZE"); + col.nullable = rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable; + try { + Object autoIncrement = rs.getObject("IS_AUTOINCREMENT"); + if (autoIncrement instanceof Boolean) { + col.isAutoIncrement = (Boolean) autoIncrement; + } else if (autoIncrement instanceof String) { + String val = autoIncrement.toString().toLowerCase(); + col.isAutoIncrement = val.equals("true") | val.equals("yes"); + } else if (autoIncrement instanceof Number) { + Number n = (Number) autoIncrement; + col.isAutoIncrement = n.intValue() > 0; + } + } catch (SQLException s) { +// throw s; + } + if (primaryKeys.size() == 1) { + if (col.name.equalsIgnoreCase(primaryKeys.get(0))) { + col.isPrimaryKey = true; + } + } + if (!col.isAutoIncrement) { + col.defaultValue = rs.getString("COLUMN_DEF"); + } + columns.put(col.name.toLowerCase(), col); + } + } finally { + closeSilently(rs); + } + } + + /** + * Generates a model (class definition) from this table. The model includes + * indexes, primary keys, default values, lengths, and nullables. + * information. + *

+ * The caller may optionally set a destination package name, whether or not + * to include the schema name (setting schema can be a problem when using + * the model between databases), and if to automatically trim strings for + * those that have a maximum length. + *

+ * + * @param packageName + * @param annotateSchema + * @param trimStrings + * @return a complete model (class definition) for this table as a string + */ + String generateModel(String packageName, boolean annotateSchema, boolean trimStrings) { + + // import statements + Set imports = Utils.newHashSet(); + imports.add(Serializable.class.getCanonicalName()); + imports.add(IQSchema.class.getCanonicalName()); + imports.add(IQTable.class.getCanonicalName()); + imports.add(IQIndexes.class.getCanonicalName()); + imports.add(IQIndex.class.getCanonicalName()); + imports.add(IQColumn.class.getCanonicalName()); + imports.add(IndexType.class.getCanonicalName()); + + // fields + StringBuilder fields = new StringBuilder(); + List sortedColumns = Utils.newArrayList(columns.values()); + Collections.sort(sortedColumns); + for (ColumnInspector col : sortedColumns) { + fields.append(generateColumn(imports, col, trimStrings)); + } + + // build complete class definition + StringBuilder model = new StringBuilder(); + if (!isNullOrEmpty(packageName)) { + // package + model.append("package " + packageName + ";"); + model.append(eol).append(eol); + } + + // imports + List sortedImports = new ArrayList(imports); + Collections.sort(sortedImports); + for (String imp : sortedImports) { + model.append("import ").append(imp).append(';').append(eol); + } + model.append(eol); + + // @IQSchema + if (annotateSchema && !isNullOrEmpty(schema)) { + model.append('@').append(IQSchema.class.getSimpleName()); + model.append('('); + AnnotationBuilder ap = new AnnotationBuilder(); + ap.addParameter(null, schema); + model.append(ap); + model.append(')').append(eol); + } + + // @IQTable + model.append('@').append(IQTable.class.getSimpleName()); + model.append('('); + + // IQTable annotation parameters + AnnotationBuilder ap = new AnnotationBuilder(); + ap.addParameter("name", table); + + if (primaryKeys.size() > 1) { + ap.addParameter("primaryKey", primaryKeys); + } + + // finish @IQTable annotation + model.append(ap); + model.append(')').append(eol); + + // @IQIndexes + // @IQIndex + String indexAnnotations = generateIndexAnnotations(); + if (!StringUtils.isNullOrEmpty(indexAnnotations)) { + model.append(indexAnnotations); + } + + // class declaration + String clazzName = ModelUtils.convertTableToClassName(table); + model.append(format("public class {0} implements Serializable '{'", clazzName)).append(eol); + model.append(eol); + model.append("\tprivate static final long serialVersionUID = 1L;").append(eol); + model.append(eol); + + // field declarations + model.append(fields); + + // default constructor + model.append("\t" + "public ").append(clazzName).append("() {").append(eol); + model.append("\t}").append(eol); + + // end of class body + model.append('}'); + model.trimToSize(); + return model.toString(); + } + + /** + * Generates the specified index annotation. + * + * @param ap + */ + String generateIndexAnnotations() { + if (indexes == null || indexes.size() == 0) { + // no matching indexes + return null; + } + AnnotationBuilder ap = new AnnotationBuilder(); + if (indexes.size() == 1) { + // single index + IndexInspector index = indexes.values().toArray(new IndexInspector[1])[0]; + ap.append(generateIndexAnnotation(index)); + ap.append(eol); + } else { + // multiple indexes + ap.append('@').append(IQIndexes.class.getSimpleName()); + ap.append("({"); + ap.resetCount(); + for (IndexInspector index : indexes.values()) { + ap.appendExceptFirst(", "); + ap.append(generateIndexAnnotation(index)); + } + ap.append("})").append(eol); + } + return ap.toString(); + } + + private String generateIndexAnnotation(IndexInspector index) { + AnnotationBuilder ap = new AnnotationBuilder(); + ap.append('@').append(IQIndex.class.getSimpleName()); + ap.append('('); + ap.resetCount(); + if (!StringUtils.isNullOrEmpty(index.name)) { + ap.addParameter("name", index.name); + } + if (!index.type.equals(IndexType.STANDARD)) { + ap.addEnum("type", index.type); + } + if (ap.getCount() > 0) { + // multiple fields specified + ap.addParameter("value", index.columns); + } else { + // default value + ap.addParameter(null, index.columns); + } + ap.append(')'); + return ap.toString(); + } + + private StatementBuilder generateColumn(Set imports, ColumnInspector col, boolean trimStrings) { + StatementBuilder sb = new StatementBuilder(); + Class clazz = col.clazz; + String column = ModelUtils.convertColumnToFieldName(col.name.toLowerCase()); + sb.append('\t'); + if (clazz == null) { + // unsupported type + clazz = Object.class; + sb.append("// unsupported type " + col.type); + } else { + // Imports + // don't import primitives, java.lang classes, or byte [] + if (clazz.getPackage() == null) { + } else if (clazz.getPackage().getName().equals("java.lang")) { + } else if (clazz.equals(byte[].class)) { + } else { + imports.add(clazz.getCanonicalName()); + } + // @IQColumn + sb.append('@').append(IQColumn.class.getSimpleName()); + + // IQColumn annotation parameters + AnnotationBuilder ap = new AnnotationBuilder(); + + // IQColumn.name + if (!col.name.equalsIgnoreCase(column)) { + ap.addParameter("name", col.name); + } + + // IQColumn.primaryKey + // composite primary keys are annotated on the table + if (col.isPrimaryKey && primaryKeys.size() == 1) { + ap.addParameter("primaryKey=true"); + } + + // IQColumn.length + if ((clazz == String.class) && (col.size > 0) && (col.size < Integer.MAX_VALUE)) { + ap.addParameter("length", col.size); + + // IQColumn.trim + if (trimStrings) { + ap.addParameter("trim=true"); + } + } else { + // IQColumn.AutoIncrement + if (col.isAutoIncrement) { + ap.addParameter("autoIncrement=true"); + } + } + + // IQColumn.nullable + if (!col.nullable) { + ap.addParameter("nullable=false"); + } + + // IQColumn.defaultValue + if (!isNullOrEmpty(col.defaultValue)) { + ap.addParameter("defaultValue=\"" + col.defaultValue + "\""); + } + + // add leading and trailing () + if (ap.length() > 0) { + ap.insert(0, '('); + ap.append(')'); + } + sb.append(ap); + } + sb.append(eol); + + // variable declaration + sb.append("\t" + "public "); + sb.append(clazz.getSimpleName()); + sb.append(' '); + sb.append(column); + sb.append(';'); + sb.append(eol).append(eol); + return sb; + } + + /** + * Validates that a table definition (annotated, interface, or both) matches + * the current state of the table and indexes in the database. Results are + * returned as a list of validation remarks which includes recommendations, + * warnings, and errors about the model. The caller may choose to have + * validate throw an exception on any validation ERROR. + * + * @param def + * the table definition + * @param throwError + * whether or not to throw an exception if an error was found + * @return a list if validation remarks + */ + List validate(TableDefinition def, boolean throwError) { + List remarks = Utils.newArrayList(); + + // model class definition validation + if (!Modifier.isPublic(def.getModelClass().getModifiers())) { + remarks.add(error(table, "SCHEMA", + format("Class {0} MUST BE PUBLIC!", def.getModelClass().getCanonicalName())).throwError( + throwError)); + } + + // Schema Validation + if (!isNullOrEmpty(schema)) { + if (isNullOrEmpty(def.schemaName)) { + remarks.add(consider(table, "SCHEMA", + format("@{0}(\"{1}\")", IQSchema.class.getSimpleName(), schema))); + } else if (!schema.equalsIgnoreCase(def.schemaName)) { + remarks.add(error( + table, + "SCHEMA", + format("@{0}(\"{1}\") != {2}", IQSchema.class.getSimpleName(), def.schemaName, schema)) + .throwError(throwError)); + } + } + + // index validation + for (IndexInspector index : indexes.values()) { + validate(remarks, def, index, throwError); + } + + // field column validation + for (FieldDefinition fieldDef : def.getFields()) { + validate(remarks, fieldDef, throwError); + } + return remarks; + } + + /** + * Validates an inspected index from the database against the + * IndexDefinition within the TableDefinition. + */ + private void validate(List remarks, TableDefinition def, IndexInspector index, + boolean throwError) { + List defIndexes = def.getIndexes(); + if (defIndexes.size() > indexes.size()) { + remarks.add(warn(table, IndexType.STANDARD.name(), "More model indexes than database indexes")); + } else if (defIndexes.size() < indexes.size()) { + remarks.add(warn(table, IndexType.STANDARD.name(), "Model class is missing indexes")); + } + // TODO complete index validation. + // need to actually compare index types and columns within each index. + + // TODO add constraints validation + List defContraintsU = def.getContraintsUnique(); + List defContraintsFK = def.getContraintsForeignKey(); + } + + /** + * Validates a column against the model's field definition. Checks for + * existence, supported type, type mapping, default value, defined lengths, + * primary key, autoincrement. + */ + private void validate(List remarks, FieldDefinition fieldDef, boolean throwError) { + // unknown field + if (!columns.containsKey(fieldDef.columnName.toLowerCase())) { + // unknown column mapping + remarks.add(error(table, fieldDef, "Does not exist in database!").throwError(throwError)); + return; + } + ColumnInspector col = columns.get(fieldDef.columnName.toLowerCase()); + Class fieldClass = fieldDef.field.getType(); + Class jdbcClass = ModelUtils.getClassForSqlType(col.type, dateTimeClass); + + // supported type check + // iciql maps to VARCHAR for unsupported types. + if (fieldDef.dataType.equals("VARCHAR") && (fieldClass != String.class)) { + remarks.add(error(table, fieldDef, + "iciql does not currently implement support for " + fieldClass.getName()).throwError( + throwError)); + } + // number types + if (!fieldClass.equals(jdbcClass)) { + if (Number.class.isAssignableFrom(fieldClass)) { + remarks.add(warn( + table, + col, + format("Precision mismatch: ModelObject={0}, ColumnObject={1}", + fieldClass.getSimpleName(), jdbcClass.getSimpleName()))); + } else { + if (!Date.class.isAssignableFrom(jdbcClass)) { + remarks.add(warn( + table, + col, + format("Object Mismatch: ModelObject={0}, ColumnObject={1}", + fieldClass.getSimpleName(), jdbcClass.getSimpleName()))); + } + } + } + + // string types + if (fieldClass == String.class) { + if ((fieldDef.length != col.size) && (col.size < Integer.MAX_VALUE)) { + remarks.add(warn( + table, + col, + format("{0}.length={1}, ColumnMaxLength={2}", IQColumn.class.getSimpleName(), + fieldDef.length, col.size))); + } + if (fieldDef.length > 0 && !fieldDef.trim) { + remarks.add(consider(table, col, format("{0}.trim=true will prevent IciqlExceptions on" + + " INSERT or UPDATE, but will clip data!", IQColumn.class.getSimpleName()))); + } + } + + // numeric autoIncrement + if (fieldDef.isAutoIncrement != col.isAutoIncrement) { + remarks.add(warn( + table, + col, + format("{0}.autoIncrement={1}" + " while Column autoIncrement={2}", + IQColumn.class.getSimpleName(), fieldDef.isAutoIncrement, col.isAutoIncrement))); + } + // default value + if (!col.isAutoIncrement && !col.isPrimaryKey) { + String defaultValue = null; + if (fieldDef.defaultValue != null && fieldDef.defaultValue instanceof String) { + defaultValue = fieldDef.defaultValue.toString(); + } + // check Model.defaultValue format + if (!ModelUtils.isProperlyFormattedDefaultValue(defaultValue)) { + remarks.add(error( + table, + col, + format("{0}.defaultValue=\"{1}\"" + " is improperly formatted!", + IQColumn.class.getSimpleName(), defaultValue)).throwError(throwError)); + // next field + return; + } + // compare Model.defaultValue to Column.defaultValue + if (isNullOrEmpty(defaultValue) && !isNullOrEmpty(col.defaultValue)) { + // Model.defaultValue is NULL, Column.defaultValue is NOT NULL + remarks.add(warn( + table, + col, + format("{0}.defaultValue=\"\"" + " while column default=\"{1}\"", + IQColumn.class.getSimpleName(), col.defaultValue))); + } else if (!isNullOrEmpty(defaultValue) && isNullOrEmpty(col.defaultValue)) { + // Column.defaultValue is NULL, Model.defaultValue is NOT NULL + remarks.add(warn( + table, + col, + format("{0}.defaultValue=\"{1}\"" + " while column default=\"\"", + IQColumn.class.getSimpleName(), defaultValue))); + } else if (!isNullOrEmpty(defaultValue) && !isNullOrEmpty(col.defaultValue)) { + if (!defaultValue.equals(col.defaultValue)) { + // Model.defaultValue != Column.defaultValue + remarks.add(warn( + table, + col, + format("{0}.defaultValue=\"{1}\"" + " while column default=\"{2}\"", + IQColumn.class.getSimpleName(), defaultValue, col.defaultValue))); + } + } + + // sanity check Model.defaultValue literal value + if (!ModelUtils.isValidDefaultValue(fieldDef.field.getType(), defaultValue)) { + remarks.add(error( + table, + col, + format("{0}.defaultValue=\"{1}\" is invalid!", IQColumn.class.getSimpleName(), + defaultValue))); + } + } + } + + /** + * Represents an index as it exists in the database. + */ + private static class IndexInspector { + + String name; + IndexType type; + private List columns = new ArrayList(); + + public IndexInspector(ResultSet rs) throws SQLException { + name = rs.getString("INDEX_NAME"); + + // determine index type + boolean hash = rs.getInt("TYPE") == DatabaseMetaData.tableIndexHashed; + boolean unique = !rs.getBoolean("NON_UNIQUE"); + + if (!hash && !unique) { + type = IndexType.STANDARD; + } else if (hash && unique) { + type = IndexType.UNIQUE_HASH; + } else if (unique) { + type = IndexType.UNIQUE; + } else if (hash) { + type = IndexType.HASH; + } + columns.add(rs.getString("COLUMN_NAME")); + } + + public void addColumn(ResultSet rs) throws SQLException { + columns.add(rs.getString("COLUMN_NAME")); + } + } + + /** + * Represents a column as it exists in the database. + */ + static class ColumnInspector implements Comparable { + String name; + String type; + int size; + boolean nullable; + Class clazz; + boolean isPrimaryKey; + boolean isAutoIncrement; + String defaultValue; + + public int compareTo(ColumnInspector o) { + if (isPrimaryKey && o.isPrimaryKey) { + // both primary sort by name + return name.compareTo(o.name); + } else if (isPrimaryKey && !o.isPrimaryKey) { + // primary first + return -1; + } else if (!isPrimaryKey && o.isPrimaryKey) { + // primary first + return 1; + } else { + // neither primary, sort by name + return name.compareTo(o.name); + } + } + } + + /** + * Convenience class based on StatementBuilder for creating the annotation + * parameter list. + */ + private static class AnnotationBuilder extends StatementBuilder { + + AnnotationBuilder() { + super(); + } + + void addParameter(String parameter) { + + appendExceptFirst(", "); + append(parameter); + } + + void addParameter(String parameter, T value) { + appendExceptFirst(", "); + if (!StringUtils.isNullOrEmpty(parameter)) { + append(parameter); + append('='); + } + if (value instanceof List) { + append("{ "); + List list = (List) value; + StatementBuilder flat = new StatementBuilder(); + for (Object o : list) { + flat.appendExceptFirst(", "); + if (o instanceof String) { + flat.append('\"'); + } + // TODO escape string + flat.append(o.toString().trim()); + if (o instanceof String) { + flat.append('\"'); + } + } + append(flat); + append(" }"); + } else { + if (value instanceof String) { + append('\"'); + } + // TODO escape + append(value.toString().trim()); + if (value instanceof String) { + append('\"'); + } + } + } + + void addEnum(String parameter, Enum value) { + appendExceptFirst(", "); + if (!StringUtils.isNullOrEmpty(parameter)) { + append(parameter); + append('='); + } + append(value.getClass().getSimpleName() + "." + value.name()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/iciql/TestCondition.java b/src/main/java/com/iciql/TestCondition.java new file mode 100644 index 0000000..010f5a1 --- /dev/null +++ b/src/main/java/com/iciql/TestCondition.java @@ -0,0 +1,115 @@ +/* + * 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 com.iciql.util.Utils; + +/** + * This class represents an incomplete condition. + * + * @param + * the incomplete condition data type + */ + +public class TestCondition { + + private A x; + + public TestCondition(A x) { + this.x = x; + } + + public Boolean is(A y) { + Boolean o = Utils.newObject(Boolean.class); + return Db.registerToken(o, new Function("=", x, y) { + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("("); + query.appendSQL(stat, null, x[0]); + stat.appendSQL(" = "); + query.appendSQL(stat, x[0], x[1]); + stat.appendSQL(")"); + } + }); + } + + public Boolean exceeds(A y) { + Boolean o = Utils.newObject(Boolean.class); + return Db.registerToken(o, new Function(">", x, y) { + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("("); + query.appendSQL(stat, null, x[0]); + stat.appendSQL(" > "); + query.appendSQL(stat, x[0], x[1]); + stat.appendSQL(")"); + } + }); + } + + public Boolean atLeast(A y) { + Boolean o = Utils.newObject(Boolean.class); + return Db.registerToken(o, new Function(">=", x, y) { + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("("); + query.appendSQL(stat, null, x[0]); + stat.appendSQL(" >= "); + query.appendSQL(stat, x[0], x[1]); + stat.appendSQL(")"); + } + }); + } + + public Boolean lessThan(A y) { + Boolean o = Utils.newObject(Boolean.class); + return Db.registerToken(o, new Function("<", x, y) { + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("("); + query.appendSQL(stat, null, x[0]); + stat.appendSQL(" < "); + query.appendSQL(stat, x[0], x[1]); + stat.appendSQL(")"); + } + }); + } + + public Boolean atMost(A y) { + Boolean o = Utils.newObject(Boolean.class); + return Db.registerToken(o, new Function("<=", x, y) { + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("("); + query.appendSQL(stat, null, x[0]); + stat.appendSQL(" <= "); + query.appendSQL(stat, x[0], x[1]); + stat.appendSQL(")"); + } + }); + } + + public Boolean like(A pattern) { + Boolean o = Utils.newObject(Boolean.class); + return Db.registerToken(o, new Function("LIKE", x, pattern) { + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("("); + query.appendSQL(stat, null, x[0]); + stat.appendSQL(" LIKE "); + query.appendSQL(stat, x[0], x[1]); + stat.appendSQL(")"); + } + }); + } + +} diff --git a/src/main/java/com/iciql/Token.java b/src/main/java/com/iciql/Token.java new file mode 100644 index 0000000..cc2203c --- /dev/null +++ b/src/main/java/com/iciql/Token.java @@ -0,0 +1,35 @@ +/* + * 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; + +/** + * Classes implementing this interface can be used as a token in a statement. + */ +public interface Token { + /** + * Append the SQL to the given statement using the given query. + * + * @param stat + * the statement to append the SQL to + * @param query + * the query to use + */ + + void appendSQL(SQLStatement stat, Query query); + +} diff --git a/src/main/java/com/iciql/UpdateColumn.java b/src/main/java/com/iciql/UpdateColumn.java new file mode 100644 index 0000000..1eaf14c --- /dev/null +++ b/src/main/java/com/iciql/UpdateColumn.java @@ -0,0 +1,35 @@ +/* + * 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; + +/** + * Classes implementing this interface can be used as a declaration in an update + * statement. + */ +public interface UpdateColumn { + + /** + * Append the SQL to the given statement using the given query. + * + * @param stat + * the statement to append the SQL to + */ + + void appendSQL(SQLStatement stat); + +} diff --git a/src/main/java/com/iciql/UpdateColumnIncrement.java b/src/main/java/com/iciql/UpdateColumnIncrement.java new file mode 100644 index 0000000..143ce48 --- /dev/null +++ b/src/main/java/com/iciql/UpdateColumnIncrement.java @@ -0,0 +1,55 @@ +/* + * 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; + +/** + * This class represents "SET column = (column + x)" in an UPDATE statement. + * + * @param + * the query type + * @param + * the new value data type + */ + +public class UpdateColumnIncrement implements UpdateColumn { + + private Query query; + private A x; + private A y; + + UpdateColumnIncrement(Query query, A x) { + this.query = query; + this.x = x; + } + + public Query by(A y) { + query.addUpdateColumnDeclaration(this); + this.y = y; + return query; + } + + public void appendSQL(SQLStatement stat) { + query.appendSQL(stat, null, x); + stat.appendSQL("=("); + query.appendSQL(stat, null, x); + stat.appendSQL("+"); + query.appendSQL(stat, x, y); + stat.appendSQL(")"); + } + +} diff --git a/src/main/java/com/iciql/UpdateColumnSet.java b/src/main/java/com/iciql/UpdateColumnSet.java new file mode 100644 index 0000000..a961480 --- /dev/null +++ b/src/main/java/com/iciql/UpdateColumnSet.java @@ -0,0 +1,63 @@ +/* + * 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; + +/** + * This class represents "SET column = value" in an UPDATE statement. + * + * @param + * the query type + * @param + * the new value data type + */ + +public class UpdateColumnSet implements UpdateColumn { + + private Query query; + private A x; + private A y; + private boolean isParameter; + + UpdateColumnSet(Query query, A x) { + this.query = query; + this.x = x; + } + + public Query to(A y) { + query.addUpdateColumnDeclaration(this); + this.y = y; + return query; + } + + public Query toParameter() { + query.addUpdateColumnDeclaration(this); + isParameter = true; + return query; + } + + public void appendSQL(SQLStatement stat) { + query.appendSQL(stat, null, x); + stat.appendSQL(" = "); + if (isParameter) { + query.appendSQL(stat, x, RuntimeParameter.PARAMETER); + } else { + query.appendSQL(stat, x, y); + } + } + +} diff --git a/src/main/java/com/iciql/ValidationRemark.java b/src/main/java/com/iciql/ValidationRemark.java new file mode 100644 index 0000000..33320ab --- /dev/null +++ b/src/main/java/com/iciql/ValidationRemark.java @@ -0,0 +1,127 @@ +/* + * 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 com.iciql.TableDefinition.FieldDefinition; +import com.iciql.TableInspector.ColumnInspector; +import com.iciql.util.StringUtils; + +/** + * A validation remark is a result of running a model validation. Each remark + * has a level, associated component (schema, table, column, index), and a + * message. + */ +public class ValidationRemark { + + /** + * The validation message level. + */ + public static enum Level { + CONSIDER, WARN, ERROR; + } + + public final Level level; + public final String table; + public final String fieldType; + public final String fieldName; + public final String message; + + private ValidationRemark(Level level, String table, String type, String message) { + this.level = level; + this.table = table; + this.fieldType = type; + this.fieldName = ""; + this.message = message; + } + + private ValidationRemark(Level level, String table, FieldDefinition field, String message) { + this.level = level; + this.table = table; + this.fieldType = field.dataType; + this.fieldName = field.columnName; + this.message = message; + } + + private ValidationRemark(Level level, String table, ColumnInspector col, String message) { + this.level = level; + this.table = table; + this.fieldType = col.type; + this.fieldName = col.name; + this.message = message; + } + + public static ValidationRemark consider(String table, String type, String message) { + return new ValidationRemark(Level.CONSIDER, table, type, message); + } + + public static ValidationRemark consider(String table, ColumnInspector col, String message) { + return new ValidationRemark(Level.CONSIDER, table, col, message); + } + + public static ValidationRemark warn(String table, ColumnInspector col, String message) { + return new ValidationRemark(Level.WARN, table, col, message); + } + + public static ValidationRemark warn(String table, String type, String message) { + return new ValidationRemark(Level.WARN, table, type, message); + } + + public static ValidationRemark error(String table, ColumnInspector col, String message) { + return new ValidationRemark(Level.ERROR, table, col, message); + } + + public static ValidationRemark error(String table, String type, String message) { + return new ValidationRemark(Level.ERROR, table, type, message); + } + + public static ValidationRemark error(String table, FieldDefinition field, String message) { + return new ValidationRemark(Level.ERROR, table, field, message); + } + + public ValidationRemark throwError(boolean throwOnError) { + if (throwOnError && isError()) { + throw new IciqlException(toString()); + } + return this; + } + + public boolean isError() { + return level.equals(Level.ERROR); + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(StringUtils.pad(level.name(), 9, " ", true)); + sb.append(StringUtils.pad(table, 25, " ", true)); + sb.append(StringUtils.pad(fieldName, 20, " ", true)); + sb.append(' '); + sb.append(message); + return sb.toString(); + } + + public String toCSVString() { + StringBuilder sb = new StringBuilder(); + sb.append(level.name()).append(','); + sb.append(table).append(','); + sb.append(fieldType).append(','); + sb.append(fieldName).append(','); + sb.append(message); + return sb.toString(); + } + +} diff --git a/src/main/java/com/iciql/bytecode/And.java b/src/main/java/com/iciql/bytecode/And.java new file mode 100644 index 0000000..808a9c1 --- /dev/null +++ b/src/main/java/com/iciql/bytecode/And.java @@ -0,0 +1,46 @@ +/* + * 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.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * An AND expression. + */ +public class And implements Token { + + private final Token left, right; + + private And(Token left, Token right) { + this.left = left; + this.right = right; + } + + static And get(Token left, Token right) { + return new And(left, right); + } + + public void appendSQL(SQLStatement stat, Query query) { + left.appendSQL(stat, query); + stat.appendSQL(" AND "); + right.appendSQL(stat, query); + } + +} diff --git a/src/main/java/com/iciql/bytecode/ArrayGet.java b/src/main/java/com/iciql/bytecode/ArrayGet.java new file mode 100644 index 0000000..29516c2 --- /dev/null +++ b/src/main/java/com/iciql/bytecode/ArrayGet.java @@ -0,0 +1,49 @@ +/* + * 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.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * An array access operation. + */ +public class ArrayGet implements Token { + + private final Token variable; + private final Token index; + + private ArrayGet(Token variable, Token index) { + this.variable = variable; + this.index = index; + } + + static ArrayGet get(Token variable, Token index) { + return new ArrayGet(variable, index); + } + + public void appendSQL(SQLStatement stat, Query query) { + // untested + variable.appendSQL(stat, query); + stat.appendSQL("["); + index.appendSQL(stat, query); + stat.appendSQL("]"); + } + +} diff --git a/src/main/java/com/iciql/bytecode/CaseWhen.java b/src/main/java/com/iciql/bytecode/CaseWhen.java new file mode 100644 index 0000000..2a1d69e --- /dev/null +++ b/src/main/java/com/iciql/bytecode/CaseWhen.java @@ -0,0 +1,62 @@ +/* + * 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.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * A conditional expression. + */ +public class CaseWhen implements Token { + + private final Token condition, ifTrue, ifFalse; + + private CaseWhen(Token condition, Token ifTrue, Token ifFalse) { + this.condition = condition; + this.ifTrue = ifTrue; + this.ifFalse = ifFalse; + } + + static Token get(Token condition, Token ifTrue, Token ifFalse) { + if ("0".equals(ifTrue.toString()) && "1".equals(ifFalse.toString())) { + return Not.get(condition); + } else if ("1".equals(ifTrue.toString()) && "0".equals(ifFalse.toString())) { + return condition; + } else if ("0".equals(ifTrue.toString())) { + return And.get(Not.get(condition), ifFalse); + } + return new CaseWhen(condition, ifTrue, ifFalse); + } + + public String toString() { + return "CASEWHEN(" + condition + ", " + ifTrue + ", " + ifFalse + ")"; + } + + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("CASEWHEN "); + condition.appendSQL(stat, query); + stat.appendSQL(" THEN "); + ifTrue.appendSQL(stat, query); + stat.appendSQL(" ELSE "); + ifFalse.appendSQL(stat, query); + stat.appendSQL(" END"); + } + +} diff --git a/src/main/java/com/iciql/bytecode/ClassReader.java b/src/main/java/com/iciql/bytecode/ClassReader.java new file mode 100644 index 0000000..38fd2f5 --- /dev/null +++ b/src/main/java/com/iciql/bytecode/ClassReader.java @@ -0,0 +1,1457 @@ +/* + * 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.bytecode; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; + +import com.iciql.IciqlException; +import com.iciql.Token; + +/** + * This class converts a method to a SQL Token by interpreting (decompiling) the + * bytecode of the class. + */ +public class ClassReader { + + private static final boolean DEBUG = false; + + private byte[] data; + private int pos; + private Constant[] constantPool; + private int startByteCode; + private String methodName; + + private String convertMethodName; + private Token result; + private Stack stack = new Stack(); + private ArrayList variables = new ArrayList(); + private boolean endOfMethod; + private boolean condition; + private int nextPc; + private Map fieldMap = new HashMap(); + + private static void debug(String s) { + if (DEBUG) { + System.out.println(s); + } + } + + public Token decompile(Object instance, Map fields, String method) { + this.fieldMap = fields; + this.convertMethodName = method; + Class clazz = instance.getClass(); + String className = clazz.getName(); + debug("class name " + className); + ByteArrayOutputStream buff = new ByteArrayOutputStream(); + try { + InputStream in = clazz.getClassLoader().getResource(className.replace('.', '/') + ".class") + .openStream(); + while (true) { + int x = in.read(); + if (x < 0) { + break; + } + buff.write(x); + } + } catch (IOException e) { + throw new IciqlException("Could not read class bytecode", e); + } + data = buff.toByteArray(); + int header = readInt(); + debug("header: " + Integer.toHexString(header)); + int minorVersion = readShort(); + int majorVersion = readShort(); + debug("version: " + majorVersion + "." + minorVersion); + int constantPoolCount = readShort(); + constantPool = new Constant[constantPoolCount]; + for (int i = 1; i < constantPoolCount; i++) { + int type = readByte(); + switch (type) { + case 1: + constantPool[i] = ConstantString.get(readString()); + break; + case 3: { + int x = readInt(); + constantPool[i] = ConstantNumber.get(x); + break; + } + case 4: { + int x = readInt(); + constantPool[i] = ConstantNumber.get("" + Float.intBitsToFloat(x), x, Constant.Type.FLOAT); + break; + } + case 5: { + long x = readLong(); + constantPool[i] = ConstantNumber.get(x); + i++; + break; + } + case 6: { + long x = readLong(); + constantPool[i] = ConstantNumber + .get("" + Double.longBitsToDouble(x), x, Constant.Type.DOUBLE); + i++; + break; + } + case 7: { + int x = readShort(); + constantPool[i] = ConstantNumber.get(null, x, ConstantNumber.Type.CLASS_REF); + break; + } + case 8: { + int x = readShort(); + constantPool[i] = ConstantNumber.get(null, x, ConstantNumber.Type.STRING_REF); + break; + } + case 9: { + int x = readInt(); + constantPool[i] = ConstantNumber.get(null, x, ConstantNumber.Type.FIELD_REF); + break; + } + case 10: { + int x = readInt(); + constantPool[i] = ConstantNumber.get(null, x, ConstantNumber.Type.METHOD_REF); + break; + } + case 11: { + int x = readInt(); + constantPool[i] = ConstantNumber.get(null, x, ConstantNumber.Type.INTERFACE_METHOD_REF); + break; + } + case 12: { + int x = readInt(); + constantPool[i] = ConstantNumber.get(null, x, ConstantNumber.Type.NAME_AND_TYPE); + break; + } + default: + throw new IciqlException("Unsupported constant pool tag: " + type); + } + } + int accessFlags = readShort(); + debug("access flags: " + accessFlags); + int classRef = readShort(); + debug("class: " + constantPool[constantPool[classRef].intValue()]); + int superClassRef = readShort(); + debug(" extends " + constantPool[constantPool[superClassRef].intValue()]); + int interfaceCount = readShort(); + for (int i = 0; i < interfaceCount; i++) { + int interfaceRef = readShort(); + debug(" implements " + constantPool[constantPool[interfaceRef].intValue()]); + } + int fieldCount = readShort(); + for (int i = 0; i < fieldCount; i++) { + readField(); + } + int methodCount = readShort(); + for (int i = 0; i < methodCount; i++) { + readMethod(); + } + readAttributes(); + return result; + } + + private void readField() { + int accessFlags = readShort(); + int nameIndex = readShort(); + int descIndex = readShort(); + debug(" " + constantPool[descIndex] + " " + constantPool[nameIndex] + " " + accessFlags); + readAttributes(); + } + + private void readMethod() { + int accessFlags = readShort(); + int nameIndex = readShort(); + int descIndex = readShort(); + String desc = constantPool[descIndex].toString(); + methodName = constantPool[nameIndex].toString(); + debug(" " + desc + " " + methodName + " " + accessFlags); + readAttributes(); + } + + private void readAttributes() { + int attributeCount = readShort(); + for (int i = 0; i < attributeCount; i++) { + int attributeNameIndex = readShort(); + String attributeName = constantPool[attributeNameIndex].toString(); + debug(" attribute " + attributeName); + int attributeLength = readInt(); + int end = pos + attributeLength; + if ("Code".equals(attributeName)) { + readCode(); + } + pos = end; + } + } + + void decompile() { + int maxStack = readShort(); + int maxLocals = readShort(); + debug("stack: " + maxStack + " locals: " + maxLocals); + int codeLength = readInt(); + startByteCode = pos; + int end = pos + codeLength; + while (pos < end) { + readByteCode(); + } + debug(""); + pos = startByteCode + codeLength; + int exceptionTableLength = readShort(); + pos += 2 * exceptionTableLength; + readAttributes(); + } + + private void readCode() { + variables.clear(); + stack.clear(); + int maxStack = readShort(); + int maxLocals = readShort(); + debug("stack: " + maxStack + " locals: " + maxLocals); + int codeLength = readInt(); + startByteCode = pos; + if (methodName.startsWith(convertMethodName)) { + result = getResult(); + } + pos = startByteCode + codeLength; + int exceptionTableLength = readShort(); + pos += 2 * exceptionTableLength; + readAttributes(); + } + + private Token getResult() { + while (true) { + readByteCode(); + if (endOfMethod) { + return stack.pop(); + } + if (condition) { + Token c = stack.pop(); + Stack currentStack = new Stack(); + currentStack.addAll(stack); + ArrayList currentVariables = new ArrayList(); + currentVariables.addAll(variables); + int branch = nextPc; + Token a = getResult(); + stack = currentStack; + variables = currentVariables; + pos = branch + startByteCode; + Token b = getResult(); + if (a.equals("0") && b.equals("1")) { + return c; + } else if (a.equals("1") && b.equals("0")) { + return Not.get(c); + } else if (b.equals("0")) { + return And.get(Not.get(c), a); + } else if (a.equals("0")) { + return And.get(c, b); + } else if (b.equals("1")) { + return Or.get(c, a); + } else if (a.equals("1")) { + return And.get(Not.get(c), b); + } + return CaseWhen.get(c, b, a); + } + if (nextPc != 0) { + pos = nextPc + startByteCode; + } + } + } + + private void readByteCode() { + int startPos = pos - startByteCode; + int opCode = readByte(); + String op; + endOfMethod = false; + condition = false; + nextPc = 0; + switch (opCode) { + case 0: + op = "nop"; + break; + case 1: + op = "aconst_null"; + stack.push(Null.INSTANCE); + break; + case 2: + op = "iconst_m1"; + stack.push(ConstantNumber.get("-1")); + break; + case 3: + op = "iconst_0"; + stack.push(ConstantNumber.get("0")); + break; + case 4: + op = "iconst_1"; + stack.push(ConstantNumber.get("1")); + break; + case 5: + op = "iconst_2"; + stack.push(ConstantNumber.get("2")); + break; + case 6: + op = "iconst_3"; + stack.push(ConstantNumber.get("3")); + break; + case 7: + op = "iconst_4"; + stack.push(ConstantNumber.get("4")); + break; + case 8: + op = "iconst_5"; + stack.push(ConstantNumber.get("5")); + break; + case 9: + op = "lconst_0"; + stack.push(ConstantNumber.get("0")); + break; + case 10: + op = "lconst_1"; + stack.push(ConstantNumber.get("1")); + break; + case 11: + op = "fconst_0"; + stack.push(ConstantNumber.get("0.0")); + break; + case 12: + op = "fconst_1"; + stack.push(ConstantNumber.get("1.0")); + break; + case 13: + op = "fconst_2"; + stack.push(ConstantNumber.get("2.0")); + break; + case 14: + op = "dconst_0"; + stack.push(ConstantNumber.get("0.0")); + break; + case 15: + op = "dconst_1"; + stack.push(ConstantNumber.get("1.0")); + break; + case 16: { + int x = (byte) readByte(); + op = "bipush " + x; + stack.push(ConstantNumber.get(x)); + break; + } + case 17: { + int x = (short) readShort(); + op = "sipush " + x; + stack.push(ConstantNumber.get(x)); + break; + } + case 18: { + Token s = getConstant(readByte()); + op = "ldc " + s; + stack.push(s); + break; + } + case 19: { + Token s = getConstant(readShort()); + op = "ldc_w " + s; + stack.push(s); + break; + } + case 20: { + Token s = getConstant(readShort()); + op = "ldc2_w " + s; + stack.push(s); + break; + } + case 21: { + int x = readByte(); + op = "iload " + x; + stack.push(getVariable(x)); + break; + } + case 22: { + int x = readByte(); + op = "lload " + x; + stack.push(getVariable(x)); + break; + } + case 23: { + int x = readByte(); + op = "fload " + x; + stack.push(getVariable(x)); + break; + } + case 24: { + int x = readByte(); + op = "dload " + x; + stack.push(getVariable(x)); + break; + } + case 25: { + int x = readByte(); + op = "aload " + x; + stack.push(getVariable(x)); + break; + } + case 26: + op = "iload_0"; + stack.push(getVariable(0)); + break; + case 27: + op = "iload_1"; + stack.push(getVariable(1)); + break; + case 28: + op = "iload_2"; + stack.push(getVariable(2)); + break; + case 29: + op = "iload_3"; + stack.push(getVariable(3)); + break; + case 30: + op = "lload_0"; + stack.push(getVariable(0)); + break; + case 31: + op = "lload_1"; + stack.push(getVariable(1)); + break; + case 32: + op = "lload_2"; + stack.push(getVariable(2)); + break; + case 33: + op = "lload_3"; + stack.push(getVariable(3)); + break; + case 34: + op = "fload_0"; + stack.push(getVariable(0)); + break; + case 35: + op = "fload_1"; + stack.push(getVariable(1)); + break; + case 36: + op = "fload_2"; + stack.push(getVariable(2)); + break; + case 37: + op = "fload_3"; + stack.push(getVariable(3)); + break; + case 38: + op = "dload_0"; + stack.push(getVariable(0)); + break; + case 39: + op = "dload_1"; + stack.push(getVariable(1)); + break; + case 40: + op = "dload_2"; + stack.push(getVariable(2)); + break; + case 41: + op = "dload_3"; + stack.push(getVariable(3)); + break; + case 42: + op = "aload_0"; + stack.push(getVariable(0)); + break; + case 43: + op = "aload_1"; + stack.push(getVariable(1)); + break; + case 44: + op = "aload_2"; + stack.push(getVariable(2)); + break; + case 45: + op = "aload_3"; + stack.push(getVariable(3)); + break; + case 46: { + Token index = stack.pop(); + Token ref = stack.pop(); + op = "iaload"; + stack.push(ArrayGet.get(ref, index)); + break; + } + case 47: { + Token index = stack.pop(); + Token ref = stack.pop(); + op = "laload"; + stack.push(ArrayGet.get(ref, index)); + break; + } + case 48: { + Token index = stack.pop(); + Token ref = stack.pop(); + op = "faload"; + stack.push(ArrayGet.get(ref, index)); + break; + } + case 49: { + Token index = stack.pop(); + Token ref = stack.pop(); + op = "daload"; + stack.push(ArrayGet.get(ref, index)); + break; + } + case 50: { + Token index = stack.pop(); + Token ref = stack.pop(); + op = "aaload"; + stack.push(ArrayGet.get(ref, index)); + break; + } + case 51: { + Token index = stack.pop(); + Token ref = stack.pop(); + op = "baload"; + stack.push(ArrayGet.get(ref, index)); + break; + } + case 52: { + Token index = stack.pop(); + Token ref = stack.pop(); + op = "caload"; + stack.push(ArrayGet.get(ref, index)); + break; + } + case 53: { + Token index = stack.pop(); + Token ref = stack.pop(); + op = "saload"; + stack.push(ArrayGet.get(ref, index)); + break; + } + case 54: { + int var = readByte(); + op = "istore " + var; + setVariable(var, stack.pop()); + break; + } + case 55: { + int var = readByte(); + op = "lstore " + var; + setVariable(var, stack.pop()); + break; + } + case 56: { + int var = readByte(); + op = "fstore " + var; + setVariable(var, stack.pop()); + break; + } + case 57: { + int var = readByte(); + op = "dstore " + var; + setVariable(var, stack.pop()); + break; + } + case 58: { + int var = readByte(); + op = "astore " + var; + setVariable(var, stack.pop()); + break; + } + case 59: + op = "istore_0"; + setVariable(0, stack.pop()); + break; + case 60: + op = "istore_1"; + setVariable(1, stack.pop()); + break; + case 61: + op = "istore_2"; + setVariable(2, stack.pop()); + break; + case 62: + op = "istore_3"; + setVariable(3, stack.pop()); + break; + case 63: + op = "lstore_0"; + setVariable(0, stack.pop()); + break; + case 64: + op = "lstore_1"; + setVariable(1, stack.pop()); + break; + case 65: + op = "lstore_2"; + setVariable(2, stack.pop()); + break; + case 66: + op = "lstore_3"; + setVariable(3, stack.pop()); + break; + case 67: + op = "fstore_0"; + setVariable(0, stack.pop()); + break; + case 68: + op = "fstore_1"; + setVariable(1, stack.pop()); + break; + case 69: + op = "fstore_2"; + setVariable(2, stack.pop()); + break; + case 70: + op = "fstore_3"; + setVariable(3, stack.pop()); + break; + case 71: + op = "dstore_0"; + setVariable(0, stack.pop()); + break; + case 72: + op = "dstore_1"; + setVariable(1, stack.pop()); + break; + case 73: + op = "dstore_2"; + setVariable(2, stack.pop()); + break; + case 74: + op = "dstore_3"; + setVariable(3, stack.pop()); + break; + case 75: + op = "astore_0"; + setVariable(0, stack.pop()); + break; + case 76: + op = "astore_1"; + setVariable(1, stack.pop()); + break; + case 77: + op = "astore_2"; + setVariable(2, stack.pop()); + break; + case 78: + op = "astore_3"; + setVariable(3, stack.pop()); + break; + case 79: { + // String value = stack.pop(); + // String index = stack.pop(); + // String ref = stack.pop(); + op = "iastore"; + // TODO side effect - not supported + break; + } + case 80: + op = "lastore"; + // TODO side effect - not supported + break; + case 81: + op = "fastore"; + // TODO side effect - not supported + break; + case 82: + op = "dastore"; + // TODO side effect - not supported + break; + case 83: + op = "aastore"; + // TODO side effect - not supported + break; + case 84: + op = "bastore"; + // TODO side effect - not supported + break; + case 85: + op = "castore"; + // TODO side effect - not supported + break; + case 86: + op = "sastore"; + // TODO side effect - not supported + break; + case 87: + op = "pop"; + stack.pop(); + break; + case 88: + op = "pop2"; + // TODO currently we don't know the stack types + stack.pop(); + stack.pop(); + break; + case 89: { + op = "dup"; + Token x = stack.pop(); + stack.push(x); + stack.push(x); + break; + } + case 90: { + op = "dup_x1"; + Token a = stack.pop(); + Token b = stack.pop(); + stack.push(a); + stack.push(b); + stack.push(a); + break; + } + case 91: { + // TODO currently we don't know the stack types + op = "dup_x2"; + Token a = stack.pop(); + Token b = stack.pop(); + Token c = stack.pop(); + stack.push(a); + stack.push(c); + stack.push(b); + stack.push(a); + break; + } + case 92: { + // TODO currently we don't know the stack types + op = "dup2"; + Token a = stack.pop(); + Token b = stack.pop(); + stack.push(b); + stack.push(a); + stack.push(b); + stack.push(a); + break; + } + case 93: { + // TODO currently we don't know the stack types + op = "dup2_x1"; + Token a = stack.pop(); + Token b = stack.pop(); + Token c = stack.pop(); + stack.push(b); + stack.push(a); + stack.push(c); + stack.push(b); + stack.push(a); + break; + } + case 94: { + // TODO currently we don't know the stack types + op = "dup2_x2"; + Token a = stack.pop(); + Token b = stack.pop(); + Token c = stack.pop(); + Token d = stack.pop(); + stack.push(b); + stack.push(a); + stack.push(d); + stack.push(c); + stack.push(b); + stack.push(a); + break; + } + case 95: { + op = "swap"; + Token a = stack.pop(); + Token b = stack.pop(); + stack.push(a); + stack.push(b); + break; + } + case 96: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "iadd"; + stack.push(Operation.get(a, Operation.Type.ADD, b)); + break; + } + case 97: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "ladd"; + stack.push(Operation.get(a, Operation.Type.ADD, b)); + break; + } + case 98: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "fadd"; + stack.push(Operation.get(a, Operation.Type.ADD, b)); + break; + } + case 99: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "dadd"; + stack.push(Operation.get(a, Operation.Type.ADD, b)); + break; + } + case 100: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "isub"; + stack.push(Operation.get(a, Operation.Type.SUBTRACT, b)); + break; + } + case 101: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "lsub"; + stack.push(Operation.get(a, Operation.Type.SUBTRACT, b)); + break; + } + case 102: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "fsub"; + stack.push(Operation.get(a, Operation.Type.SUBTRACT, b)); + break; + } + case 103: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "dsub"; + stack.push(Operation.get(a, Operation.Type.SUBTRACT, b)); + break; + } + case 104: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "imul"; + stack.push(Operation.get(a, Operation.Type.MULTIPLY, b)); + break; + } + case 105: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "lmul"; + stack.push(Operation.get(a, Operation.Type.MULTIPLY, b)); + break; + } + case 106: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "fmul"; + stack.push(Operation.get(a, Operation.Type.MULTIPLY, b)); + break; + } + case 107: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "dmul"; + stack.push(Operation.get(a, Operation.Type.MULTIPLY, b)); + break; + } + case 108: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "idiv"; + stack.push(Operation.get(a, Operation.Type.DIVIDE, b)); + break; + } + case 109: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "ldiv"; + stack.push(Operation.get(a, Operation.Type.DIVIDE, b)); + break; + } + case 110: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "fdiv"; + stack.push(Operation.get(a, Operation.Type.DIVIDE, b)); + break; + } + case 111: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "ddiv"; + stack.push(Operation.get(a, Operation.Type.DIVIDE, b)); + break; + } + case 112: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "irem"; + stack.push(Operation.get(a, Operation.Type.MOD, b)); + break; + } + case 113: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "lrem"; + stack.push(Operation.get(a, Operation.Type.MOD, b)); + break; + } + case 114: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "frem"; + stack.push(Operation.get(a, Operation.Type.MOD, b)); + break; + } + case 115: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "drem"; + stack.push(Operation.get(a, Operation.Type.MOD, b)); + break; + } + // case 116: + // op = "ineg"; + // break; + // case 117: + // op = "lneg"; + // break; + // case 118: + // op = "fneg"; + // break; + // case 119: + // op = "dneg"; + // break; + // case 120: + // op = "ishl"; + // break; + // case 121: + // op = "lshl"; + // break; + // case 122: + // op = "ishr"; + // break; + // case 123: + // op = "lshr"; + // break; + // case 124: + // op = "iushr"; + // break; + // case 125: + // op = "lushr"; + // break; + // case 126: + // op = "iand"; + // break; + // case 127: + // op = "land"; + // break; + // case 128: + // op = "ior"; + // break; + // case 129: + // op = "lor"; + // break; + // case 130: + // op = "ixor"; + // break; + // case 131: + // op = "lxor"; + // break; + // case 132: { + // int var = readByte(); + // int off = (byte) readByte(); + // op = "iinc " + var + " " + off; + // break; + // } + // case 133: + // op = "i2l"; + // break; + // case 134: + // op = "i2f"; + // break; + // case 135: + // op = "i2d"; + // break; + // case 136: + // op = "l2i"; + // break; + // case 137: + // op = "l2f"; + // break; + // case 138: + // op = "l2d"; + // break; + // case 139: + // op = "f2i"; + // break; + // case 140: + // op = "f2l"; + // break; + // case 141: + // op = "f2d"; + // break; + // case 142: + // op = "d2i"; + // break; + // case 143: + // op = "d2l"; + // break; + // case 144: + // op = "d2f"; + // break; + // case 145: + // op = "i2b"; + // break; + // case 146: + // op = "i2c"; + // break; + // case 147: + // op = "i2s"; + // break; + case 148: { + Token b = stack.pop(), a = stack.pop(); + stack.push(new Function("SIGN", Operation.get(a, Operation.Type.SUBTRACT, b))); + op = "lcmp"; + break; + } + // case 149: + // op = "fcmpl"; + // break; + // case 150: + // op = "fcmpg"; + // break; + // case 151: + // op = "dcmpl"; + // break; + // case 152: + // op = "dcmpg"; + // break; + case 153: + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + stack.push(Operation.get(stack.pop(), Operation.Type.EQUALS, ConstantNumber.get(0))); + op = "ifeq " + nextPc; + break; + case 154: + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + stack.push(Operation.get(stack.pop(), Operation.Type.NOT_EQUALS, ConstantNumber.get(0))); + op = "ifne " + nextPc; + break; + case 155: + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + stack.push(Operation.get(stack.pop(), Operation.Type.SMALLER, ConstantNumber.get(0))); + op = "iflt " + nextPc; + break; + case 156: + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + stack.push(Operation.get(stack.pop(), Operation.Type.BIGGER_EQUALS, ConstantNumber.get(0))); + op = "ifge " + nextPc; + break; + case 157: + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + stack.push(Operation.get(stack.pop(), Operation.Type.BIGGER, ConstantNumber.get(0))); + op = "ifgt " + nextPc; + break; + case 158: + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + stack.push(Operation.get(stack.pop(), Operation.Type.SMALLER_EQUALS, ConstantNumber.get(0))); + op = "ifle " + nextPc; + break; + case 159: { + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + Token b = stack.pop(), a = stack.pop(); + stack.push(Operation.get(a, Operation.Type.EQUALS, b)); + op = "if_icmpeq " + nextPc; + break; + } + case 160: { + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + Token b = stack.pop(), a = stack.pop(); + stack.push(Operation.get(a, Operation.Type.NOT_EQUALS, b)); + op = "if_icmpne " + nextPc; + break; + } + case 161: { + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + Token b = stack.pop(), a = stack.pop(); + stack.push(Operation.get(a, Operation.Type.SMALLER, b)); + op = "if_icmplt " + nextPc; + break; + } + case 162: { + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + Token b = stack.pop(), a = stack.pop(); + stack.push(Operation.get(a, Operation.Type.BIGGER_EQUALS, b)); + op = "if_icmpge " + nextPc; + break; + } + case 163: { + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + Token b = stack.pop(), a = stack.pop(); + stack.push(Operation.get(a, Operation.Type.BIGGER, b)); + op = "if_icmpgt " + nextPc; + break; + } + case 164: { + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + Token b = stack.pop(), a = stack.pop(); + stack.push(Operation.get(a, Operation.Type.SMALLER_EQUALS, b)); + op = "if_icmple " + nextPc; + break; + } + case 165: { + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + Token b = stack.pop(), a = stack.pop(); + stack.push(Operation.get(a, Operation.Type.EQUALS, b)); + op = "if_acmpeq " + nextPc; + break; + } + case 166: { + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + Token b = stack.pop(), a = stack.pop(); + stack.push(Operation.get(a, Operation.Type.NOT_EQUALS, b)); + op = "if_acmpne " + nextPc; + break; + } + case 167: + nextPc = getAbsolutePos(pos, readShort()); + op = "goto " + nextPc; + break; + // case 168: + // // TODO not supported yet + // op = "jsr " + getAbsolutePos(pos, readShort()); + // break; + // case 169: + // // TODO not supported yet + // op = "ret " + readByte(); + // break; + // case 170: { + // int start = pos; + // pos += 4 - ((pos - startByteCode) & 3); + // int def = readInt(); + // int low = readInt(), high = readInt(); + // int n = high - low + 1; + // op = "tableswitch default:" + getAbsolutePos(start, def); + // StringBuilder buff = new StringBuilder(); + // for (int i = 0; i < n; i++) { + // buff.append(' ').append(low++). + // append(":"). + // append(getAbsolutePos(start, readInt())); + // } + // op += buff.toString(); + // // pos += n * 4; + // break; + // } + // case 171: { + // int start = pos; + // pos += 4 - ((pos - startByteCode) & 3); + // int def = readInt(); + // int n = readInt(); + // op = "lookupswitch default:" + getAbsolutePos(start, def); + // StringBuilder buff = new StringBuilder(); + // for (int i = 0; i < n; i++) { + // buff.append(' '). + // append(readInt()). + // append(":"). + // append(getAbsolutePos(start, readInt())); + // } + // op += buff.toString(); + // // pos += n * 8; + // break; + // } + case 172: + op = "ireturn"; + endOfMethod = true; + break; + case 173: + op = "lreturn"; + endOfMethod = true; + break; + case 174: + op = "freturn"; + endOfMethod = true; + break; + case 175: + op = "dreturn"; + endOfMethod = true; + break; + case 176: + op = "areturn"; + endOfMethod = true; + break; + case 177: + op = "return"; + // no value returned + stack.push(null); + endOfMethod = true; + break; + // case 178: + // op = "getstatic " + getField(readShort()); + // break; + // case 179: + // op = "putstatic " + getField(readShort()); + // break; + case 180: { + String field = getField(readShort()); + Token p = stack.pop(); + String s = p + "." + field.substring(field.lastIndexOf('.') + 1, field.indexOf(' ')); + if (s.startsWith("this.")) { + s = s.substring(5); + } + stack.push(Variable.get(s, fieldMap.get(s))); + op = "getfield " + field; + break; + } + // case 181: + // op = "putfield " + getField(readShort()); + // break; + case 182: { + String method = getMethod(readShort()); + op = "invokevirtual " + method; + if (method.equals("java/lang/String.equals (Ljava/lang/Object;)Z")) { + Token a = stack.pop(); + Token b = stack.pop(); + stack.push(Operation.get(a, Operation.Type.EQUALS, b)); + } else if (method.equals("java/lang/Integer.intValue ()I")) { + // ignore + } else if (method.equals("java/lang/Long.longValue ()J")) { + // ignore + } + break; + } + case 183: { + String method = getMethod(readShort()); + op = "invokespecial " + method; + break; + } + case 184: + op = "invokestatic " + getMethod(readShort()); + break; + // case 185: { + // int methodRef = readShort(); + // readByte(); + // readByte(); + // op = "invokeinterface " + getMethod(methodRef); + // break; + // } + case 187: { + String className = constantPool[constantPool[readShort()].intValue()].toString(); + op = "new " + className; + break; + } + // case 188: + // op = "newarray " + readByte(); + // break; + // case 189: + // op = "anewarray " + cpString[readShort()]; + // break; + // case 190: + // op = "arraylength"; + // break; + // case 191: + // op = "athrow"; + // break; + // case 192: + // op = "checkcast " + cpString[readShort()]; + // break; + // case 193: + // op = "instanceof " + cpString[readShort()]; + // break; + // case 194: + // op = "monitorenter"; + // break; + // case 195: + // op = "monitorexit"; + // break; + // case 196: { + // opCode = readByte(); + // switch (opCode) { + // case 21: + // op = "wide iload " + readShort(); + // break; + // case 22: + // op = "wide lload " + readShort(); + // break; + // case 23: + // op = "wide fload " + readShort(); + // break; + // case 24: + // op = "wide dload " + readShort(); + // break; + // case 25: + // op = "wide aload " + readShort(); + // break; + // case 54: + // op = "wide istore " + readShort(); + // break; + // case 55: + // op = "wide lstore " + readShort(); + // break; + // case 56: + // op = "wide fstore " + readShort(); + // break; + // case 57: + // op = "wide dstore " + readShort(); + // break; + // case 58: + // op = "wide astore " + readShort(); + // break; + // case 132: { + // int var = readShort(); + // int off = (short) readShort(); + // op = "wide iinc " + var + " " + off; + // break; + // } + // case 169: + // op = "wide ret " + readShort(); + // break; + // default: + // throw new IciqlException( + // "Unsupported wide opCode " + opCode); + // } + // break; + // } + // case 197: + // op = "multianewarray " + cpString[readShort()] + " " + readByte(); + // break; + // case 198: { + // condition = true; + // nextPc = getAbsolutePos(pos, readShort()); + // Token a = stack.pop(); + // stack.push("(" + a + " IS NULL)"); + // op = "ifnull " + nextPc; + // break; + // } + // case 199: { + // condition = true; + // nextPc = getAbsolutePos(pos, readShort()); + // Token a = stack.pop(); + // stack.push("(" + a + " IS NOT NULL)"); + // op = "ifnonnull " + nextPc; + // break; + // } + case 200: + op = "goto_w " + getAbsolutePos(pos, readInt()); + break; + case 201: + op = "jsr_w " + getAbsolutePos(pos, readInt()); + break; + default: + throw new IciqlException("Unsupported opCode " + opCode); + } + debug(" " + startPos + ": " + op); + } + + private void setVariable(int x, Token value) { + while (x >= variables.size()) { + variables.add(Variable.get("p" + variables.size(), null)); + } + variables.set(x, value); + } + + private Token getVariable(int x) { + if (x == 0) { + return Variable.THIS; + } + while (x >= variables.size()) { + variables.add(Variable.get("p" + variables.size(), null)); + } + return variables.get(x); + } + + private String getField(int fieldRef) { + int field = constantPool[fieldRef].intValue(); + int classIndex = field >>> 16; + int nameAndType = constantPool[field & 0xffff].intValue(); + String className = constantPool[constantPool[classIndex].intValue()] + "." + + constantPool[nameAndType >>> 16] + " " + constantPool[nameAndType & 0xffff]; + return className; + } + + private String getMethod(int methodRef) { + int method = constantPool[methodRef].intValue(); + int classIndex = method >>> 16; + int nameAndType = constantPool[method & 0xffff].intValue(); + String className = constantPool[constantPool[classIndex].intValue()] + "." + + constantPool[nameAndType >>> 16] + " " + constantPool[nameAndType & 0xffff]; + return className; + } + + private Constant getConstant(int constantRef) { + Constant c = constantPool[constantRef]; + switch (c.getType()) { + case INT: + case FLOAT: + case DOUBLE: + case LONG: + return c; + case STRING_REF: + return constantPool[c.intValue()]; + default: + throw new IciqlException("Not a constant: " + constantRef); + } + } + + private String readString() { + int size = readShort(); + byte[] buff = data; + int p = pos, end = p + size; + char[] chars = new char[size]; + int j = 0; + for (; p < end; j++) { + int x = buff[p++] & 0xff; + if (x < 0x80) { + chars[j] = (char) x; + } else if (x >= 0xe0) { + chars[j] = (char) (((x & 0xf) << 12) + ((buff[p++] & 0x3f) << 6) + (buff[p++] & 0x3f)); + } else { + chars[j] = (char) (((x & 0x1f) << 6) + (buff[p++] & 0x3f)); + } + } + pos = p; + return new String(chars, 0, j); + } + + private int getAbsolutePos(int start, int offset) { + return start - startByteCode - 1 + (short) offset; + } + + private int readByte() { + return data[pos++] & 0xff; + } + + private int readShort() { + byte[] buff = data; + return ((buff[pos++] & 0xff) << 8) + (buff[pos++] & 0xff); + } + + private int readInt() { + byte[] buff = data; + return (buff[pos++] << 24) + ((buff[pos++] & 0xff) << 16) + ((buff[pos++] & 0xff) << 8) + + (buff[pos++] & 0xff); + } + + private long readLong() { + return ((long) (readInt()) << 32) + (readInt() & 0xffffffffL); + } + +} diff --git a/src/main/java/com/iciql/bytecode/Constant.java b/src/main/java/com/iciql/bytecode/Constant.java new file mode 100644 index 0000000..65cd66b --- /dev/null +++ b/src/main/java/com/iciql/bytecode/Constant.java @@ -0,0 +1,38 @@ +/* + * 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.bytecode; + +import com.iciql.Token; + +/** + * An expression in the constant pool. + */ +public interface Constant extends Token { + + /** + * The constant pool type. + */ + enum Type { + STRING, INT, FLOAT, DOUBLE, LONG, CLASS_REF, STRING_REF, FIELD_REF, METHOD_REF, INTERFACE_METHOD_REF, NAME_AND_TYPE + } + + Constant.Type getType(); + + int intValue(); + +} diff --git a/src/main/java/com/iciql/bytecode/ConstantNumber.java b/src/main/java/com/iciql/bytecode/ConstantNumber.java new file mode 100644 index 0000000..934de3d --- /dev/null +++ b/src/main/java/com/iciql/bytecode/ConstantNumber.java @@ -0,0 +1,70 @@ +/* + * 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.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; + +/** + * A literal number. + */ +public class ConstantNumber implements Constant { + + private final String value; + private final Type type; + private final long longValue; + + private ConstantNumber(String value, long longValue, Type type) { + this.value = value; + this.longValue = longValue; + this.type = type; + } + + static ConstantNumber get(String v) { + return new ConstantNumber(v, 0, Type.STRING); + } + + static ConstantNumber get(int v) { + return new ConstantNumber("" + v, v, Type.INT); + } + + static ConstantNumber get(long v) { + return new ConstantNumber("" + v, v, Type.LONG); + } + + static ConstantNumber get(String s, long x, Type type) { + return new ConstantNumber(s, x, type); + } + + public int intValue() { + return (int) longValue; + } + + public String toString() { + return value; + } + + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL(toString()); + } + + public Constant.Type getType() { + return type; + } + +} diff --git a/src/main/java/com/iciql/bytecode/ConstantString.java b/src/main/java/com/iciql/bytecode/ConstantString.java new file mode 100644 index 0000000..985f97d --- /dev/null +++ b/src/main/java/com/iciql/bytecode/ConstantString.java @@ -0,0 +1,55 @@ +/* + * 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.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.util.StringUtils; + +/** + * A string constant. + */ +public class ConstantString implements Constant { + + private final String value; + + private ConstantString(String value) { + this.value = value; + } + + static ConstantString get(String v) { + return new ConstantString(v); + } + + public String toString() { + return value; + } + + public int intValue() { + return 0; + } + + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL(StringUtils.quoteStringSQL(value)); + } + + public Constant.Type getType() { + return Constant.Type.STRING; + } + +} diff --git a/src/main/java/com/iciql/bytecode/Function.java b/src/main/java/com/iciql/bytecode/Function.java new file mode 100644 index 0000000..56a55ea --- /dev/null +++ b/src/main/java/com/iciql/bytecode/Function.java @@ -0,0 +1,47 @@ +/* + * 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.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * A method call. + */ +class Function implements Token { + + private final String name; + private final Token expr; + + Function(String name, Token expr) { + this.name = name; + this.expr = expr; + } + + public String toString() { + return name + "(" + expr + ")"; + } + + public void appendSQL(SQLStatement stat, Query query) { + // untested + stat.appendSQL(name + "("); + expr.appendSQL(stat, query); + stat.appendSQL(")"); + } +} diff --git a/src/main/java/com/iciql/bytecode/Not.java b/src/main/java/com/iciql/bytecode/Not.java new file mode 100644 index 0000000..ab5ab84 --- /dev/null +++ b/src/main/java/com/iciql/bytecode/Not.java @@ -0,0 +1,55 @@ +/* + * 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.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * A NOT condition. + */ +public class Not implements Token { + + private Token expr; + + private Not(Token expr) { + this.expr = expr; + } + + static Token get(Token expr) { + if (expr instanceof Not) { + return ((Not) expr).expr; + } else if (expr instanceof Operation) { + return ((Operation) expr).reverse(); + } + return new Not(expr); + } + + Token not() { + return expr; + } + + public void appendSQL(SQLStatement stat, Query query) { + // untested + stat.appendSQL("NOT("); + expr.appendSQL(stat, query); + stat.appendSQL(")"); + } + +} diff --git a/src/main/java/com/iciql/bytecode/Null.java b/src/main/java/com/iciql/bytecode/Null.java new file mode 100644 index 0000000..a28de56 --- /dev/null +++ b/src/main/java/com/iciql/bytecode/Null.java @@ -0,0 +1,44 @@ +/* + * 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.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * The Java 'null'. + */ +public class Null implements Token { + + static final Null INSTANCE = new Null(); + + private Null() { + // don't allow to create new instances + } + + public String toString() { + return "null"; + } + + public void appendSQL(SQLStatement stat, Query query) { + // untested + stat.appendSQL("NULL"); + } + +} diff --git a/src/main/java/com/iciql/bytecode/Operation.java b/src/main/java/com/iciql/bytecode/Operation.java new file mode 100644 index 0000000..7cd42d9 --- /dev/null +++ b/src/main/java/com/iciql/bytecode/Operation.java @@ -0,0 +1,111 @@ +/* + * 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.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * A mathematical or comparison operation. + */ +class Operation implements Token { + + /** + * The operation type. + */ + enum Type { + EQUALS("=") { + Type reverse() { + return NOT_EQUALS; + } + }, + NOT_EQUALS("<>") { + Type reverse() { + return EQUALS; + } + }, + BIGGER(">") { + Type reverse() { + return SMALLER_EQUALS; + } + }, + BIGGER_EQUALS(">=") { + Type reverse() { + return SMALLER; + } + }, + SMALLER_EQUALS("<=") { + Type reverse() { + return BIGGER; + } + }, + SMALLER("<") { + Type reverse() { + return BIGGER_EQUALS; + } + }, + ADD("+"), SUBTRACT("-"), MULTIPLY("*"), DIVIDE("/"), MOD("%"); + + private String name; + + Type(String name) { + this.name = name; + } + + public String toString() { + return name; + } + + Type reverse() { + return null; + } + + } + + private final Token left, right; + private final Type op; + + private Operation(Token left, Type op, Token right) { + this.left = left; + this.op = op; + this.right = right; + } + + static Token get(Token left, Type op, Token right) { + if (op == Type.NOT_EQUALS && "0".equals(right.toString())) { + return left; + } + return new Operation(left, op, right); + } + + public String toString() { + return left + " " + op + " " + right; + } + + public Token reverse() { + return get(left, op.reverse(), right); + } + + public void appendSQL(SQLStatement stat, Query query) { + left.appendSQL(stat, query); + stat.appendSQL(op.toString()); + right.appendSQL(stat, query); + } + +} diff --git a/src/main/java/com/iciql/bytecode/Or.java b/src/main/java/com/iciql/bytecode/Or.java new file mode 100644 index 0000000..37da2a6 --- /dev/null +++ b/src/main/java/com/iciql/bytecode/Or.java @@ -0,0 +1,47 @@ +/* + * 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.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * An OR expression. + */ +public class Or implements Token { + + private final Token left, right; + + private Or(Token left, Token right) { + this.left = left; + this.right = right; + } + + static Or get(Token left, Token right) { + return new Or(left, right); + } + + public void appendSQL(SQLStatement stat, Query query) { + // untested + left.appendSQL(stat, query); + stat.appendSQL(" OR "); + right.appendSQL(stat, query); + } + +} diff --git a/src/main/java/com/iciql/bytecode/Variable.java b/src/main/java/com/iciql/bytecode/Variable.java new file mode 100644 index 0000000..f3dbc01 --- /dev/null +++ b/src/main/java/com/iciql/bytecode/Variable.java @@ -0,0 +1,51 @@ +/* + * 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.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * A variable. + */ +public class Variable implements Token { + + static final Variable THIS = new Variable("this", null); + + private final String name; + private final Object obj; + + private Variable(String name, Object obj) { + this.name = name; + this.obj = obj; + } + + static Variable get(String name, Object obj) { + return new Variable(name, obj); + } + + public String toString() { + return name; + } + + public void appendSQL(SQLStatement stat, Query query) { + query.appendSQL(stat, null, obj); + } + +} diff --git a/src/main/java/com/iciql/bytecode/package.html b/src/main/java/com/iciql/bytecode/package.html new file mode 100644 index 0000000..5107481 --- /dev/null +++ b/src/main/java/com/iciql/bytecode/package.html @@ -0,0 +1,25 @@ + + + + +Javadoc package documentation + + +The class decompiler for natural syntax iciql clauses. + + \ No newline at end of file diff --git a/src/main/java/com/iciql/package.html b/src/main/java/com/iciql/package.html new file mode 100644 index 0000000..769837b --- /dev/null +++ b/src/main/java/com/iciql/package.html @@ -0,0 +1,25 @@ + + + + +Javadoc package documentation + + +iciql (pronounced "icicle") is a Java JDBC SQL statement generator and simple object mapper + + \ No newline at end of file diff --git a/src/main/java/com/iciql/util/GenerateModels.java b/src/main/java/com/iciql/util/GenerateModels.java new file mode 100644 index 0000000..eac9f6c --- /dev/null +++ b/src/main/java/com/iciql/util/GenerateModels.java @@ -0,0 +1,193 @@ +/* + * 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.util; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.Writer; +import java.sql.SQLException; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.iciql.Db; +import com.iciql.DbInspector; + +/** + * Generates iciql models. + */ +public class GenerateModels { + + /** + * The output stream where this tool writes to. + */ + protected PrintStream out = System.out; + + public static void main(String... args) { + GenerateModels tool = new GenerateModels(); + try { + tool.runTool(args); + } catch (SQLException e) { + tool.out.print("Error: "); + tool.out.println(e.getMessage()); + tool.out.println(); + tool.showUsage(); + } + } + + public void runTool(String... args) throws SQLException { + String url = null; + String user = "sa"; + String password = ""; + String schema = null; + String table = null; + String packageName = ""; + String folder = null; + boolean annotateSchema = true; + boolean trimStrings = false; + for (int i = 0; args != null && i < args.length; i++) { + String arg = args[i]; + if (arg.equals("-url")) { + url = args[++i]; + } else if (arg.equals("-user")) { + user = args[++i]; + } else if (arg.equals("-password")) { + password = args[++i]; + } else if (arg.equals("-schema")) { + schema = args[++i]; + } else if (arg.equals("-table")) { + table = args[++i]; + } else if (arg.equals("-package")) { + packageName = args[++i]; + } else if (arg.equals("-folder")) { + folder = args[++i]; + } else if (arg.equals("-annotateSchema")) { + try { + annotateSchema = Boolean.parseBoolean(args[++i]); + } catch (Throwable t) { + throw new SQLException("Can not parse -annotateSchema value"); + } + } else if (arg.equals("-trimStrings")) { + try { + trimStrings = Boolean.parseBoolean(args[++i]); + } catch (Throwable t) { + throw new SQLException("Can not parse -trimStrings value"); + } + } else { + throwUnsupportedOption(arg); + } + } + if (url == null) { + throw new SQLException("URL not set"); + } + execute(url, user, password, schema, table, packageName, folder, annotateSchema, trimStrings); + } + + /** + * Generates models from the database. + * + * @param url + * the database URL + * @param user + * the user name + * @param password + * the password + * @param schema + * the schema to read from. null for all schemas. + * @param table + * the table to model. null for all tables within schema. + * @param packageName + * the package name of the model classes. + * @param folder + * destination folder for model classes (package path not + * included) + * @param annotateSchema + * includes the schema in the table model annotations + * @param trimStrings + * automatically trim strings that exceed maxLength + */ + public static void execute(String url, String user, String password, String schema, String table, + String packageName, String folder, boolean annotateSchema, boolean trimStrings) + throws SQLException { + try { + Db db; + if (password == null) { + db = Db.open(url, user, (String) null); + } else { + db = Db.open(url, user, password); + } + DbInspector inspector = new DbInspector(db); + List models = inspector.generateModel(schema, table, packageName, annotateSchema, + trimStrings); + File parentFile; + if (StringUtils.isNullOrEmpty(folder)) { + parentFile = new File(System.getProperty("user.dir")); + } else { + parentFile = new File(folder); + } + parentFile.mkdirs(); + Pattern p = Pattern.compile("class ([a-zA-Z0-9]+)"); + for (String model : models) { + Matcher m = p.matcher(model); + if (m.find()) { + String className = m.group().substring("class".length()).trim(); + File classFile = new File(parentFile, className + ".java"); + Writer o = new FileWriter(classFile, false); + PrintWriter writer = new PrintWriter(new BufferedWriter(o)); + writer.write(model); + writer.close(); + System.out.println("Generated " + classFile.getAbsolutePath()); + } + } + } catch (IOException io) { + throw new SQLException("could not generate model", io); + } + } + + /** + * Throw a SQLException saying this command line option is not supported. + * + * @param option + * the unsupported option + * @return this method never returns normally + */ + protected SQLException throwUnsupportedOption(String option) throws SQLException { + showUsage(); + throw new SQLException("Unsupported option: " + option); + } + + protected void showUsage() { + out.println("GenerateModels"); + out.println("Usage:"); + out.println(); + out.println("(*) -url jdbc:h2:~test"); + out.println(" -user "); + out.println(" -password "); + out.println(" -schema "); + out.println(" -table "); + out.println(" -package "); + out.println(" -folder "); + out.println(" -annotateSchema "); + out.println(" -trimStrings "); + } + +} diff --git a/src/main/java/com/iciql/util/IciqlLogger.java b/src/main/java/com/iciql/util/IciqlLogger.java new file mode 100644 index 0000000..d8005bb --- /dev/null +++ b/src/main/java/com/iciql/util/IciqlLogger.java @@ -0,0 +1,214 @@ +/* + * 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.util; + +import java.text.DecimalFormat; +import java.text.MessageFormat; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; + +import com.iciql.IciqlException; + +/** + * Utility class to optionally log generated statements to IciqlListeners.
+ * Statement logging is disabled by default. + *

+ * This class also tracks the counts for generated statements by major type. + * + */ +public class IciqlLogger { + + /** + * Enumeration of the different statement types that are logged. + */ + public enum StatementType { + STAT, TOTAL, CREATE, INSERT, UPDATE, MERGE, DELETE, SELECT, DROP, WARN; + } + + /** + * Interface that defines an iciql listener. + */ + public interface IciqlListener { + void logIciql(StatementType type, String statement); + } + + private static final ExecutorService EXEC = Executors.newSingleThreadExecutor(); + private static final Set LISTENERS = Utils.newHashSet(); + private static final IciqlListener CONSOLE = new IciqlListener() { + + @Override + public void logIciql(StatementType type, String message) { + System.out.println(message); + } + }; + + private static final AtomicLong SELECT_COUNT = new AtomicLong(); + private static final AtomicLong CREATE_COUNT = new AtomicLong(); + private static final AtomicLong INSERT_COUNT = new AtomicLong(); + private static final AtomicLong UPDATE_COUNT = new AtomicLong(); + private static final AtomicLong MERGE_COUNT = new AtomicLong(); + private static final AtomicLong DELETE_COUNT = new AtomicLong(); + private static final AtomicLong DROP_COUNT = new AtomicLong(); + private static final AtomicLong WARN_COUNT = new AtomicLong(); + + /** + * Activates the Console Logger. + */ + public static void activateConsoleLogger() { + registerListener(CONSOLE); + } + + /** + * Deactivates the Console Logger. + */ + public static void deactivateConsoleLogger() { + unregisterListener(CONSOLE); + } + + /** + * Registers a listener with the relay. + * + * @param listener + */ + public static void registerListener(IciqlListener listener) { + LISTENERS.add(listener); + } + + /** + * Unregisters a listener with the relay. + * + * @param listener + */ + public static void unregisterListener(IciqlListener listener) { + if (!LISTENERS.remove(listener)) { + throw new IciqlException("Failed to remove iciql listener {0}", listener); + } + } + + public static void create(String statement) { + CREATE_COUNT.incrementAndGet(); + logStatement(StatementType.CREATE, statement); + } + + public static void insert(String statement) { + INSERT_COUNT.incrementAndGet(); + logStatement(StatementType.INSERT, statement); + } + + public static void update(String statement) { + UPDATE_COUNT.incrementAndGet(); + logStatement(StatementType.UPDATE, statement); + } + + public static void merge(String statement) { + MERGE_COUNT.incrementAndGet(); + logStatement(StatementType.MERGE, statement); + } + + public static void delete(String statement) { + DELETE_COUNT.incrementAndGet(); + logStatement(StatementType.DELETE, statement); + } + + public static void select(String statement) { + SELECT_COUNT.incrementAndGet(); + logStatement(StatementType.SELECT, statement); + } + + public static void drop(String statement) { + DROP_COUNT.incrementAndGet(); + logStatement(StatementType.DROP, statement); + } + + public static void warn(String message, Object... args) { + WARN_COUNT.incrementAndGet(); + logStatement(StatementType.WARN, args.length > 0 ? MessageFormat.format(message, args) : message); + } + + private static void logStatement(final StatementType type, final String statement) { + for (final IciqlListener listener : LISTENERS) { + EXEC.execute(new Runnable() { + public void run() { + listener.logIciql(type, statement); + } + }); + } + } + + public static long getCreateCount() { + return CREATE_COUNT.longValue(); + } + + public static long getInsertCount() { + return INSERT_COUNT.longValue(); + } + + public static long getUpdateCount() { + return UPDATE_COUNT.longValue(); + } + + public static long getMergeCount() { + return MERGE_COUNT.longValue(); + } + + public static long getDeleteCount() { + return DELETE_COUNT.longValue(); + } + + public static long getSelectCount() { + return SELECT_COUNT.longValue(); + } + + public static long getDropCount() { + return DROP_COUNT.longValue(); + } + + public static long getWarnCount() { + return WARN_COUNT.longValue(); + } + + public static long getTotalCount() { + return getCreateCount() + getInsertCount() + getUpdateCount() + getDeleteCount() + getMergeCount() + + getSelectCount() + getDropCount(); + } + + public static void logStats() { + logStatement(StatementType.STAT, "iciql Runtime Statistics"); + logStatement(StatementType.STAT, "========================"); + logStat(StatementType.WARN, getWarnCount()); + logStatement(StatementType.STAT, "========================"); + logStat(StatementType.CREATE, getCreateCount()); + logStat(StatementType.INSERT, getInsertCount()); + logStat(StatementType.UPDATE, getUpdateCount()); + logStat(StatementType.MERGE, getMergeCount()); + logStat(StatementType.DELETE, getDeleteCount()); + logStat(StatementType.SELECT, getSelectCount()); + logStat(StatementType.DROP, getDropCount()); + logStatement(StatementType.STAT, "========================"); + logStat(StatementType.TOTAL, getTotalCount()); + } + + private static void logStat(StatementType type, long value) { + if (value > 0) { + DecimalFormat df = new DecimalFormat("###,###,###,###"); + logStatement(StatementType.STAT, + StringUtils.pad(type.name(), 6, " ", true) + " = " + df.format(value)); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/iciql/util/JdbcUtils.java b/src/main/java/com/iciql/util/JdbcUtils.java new file mode 100644 index 0000000..4a4a2b6 --- /dev/null +++ b/src/main/java/com/iciql/util/JdbcUtils.java @@ -0,0 +1,254 @@ +/* + * 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.util; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Properties; + +import javax.naming.Context; +import javax.sql.DataSource; +import javax.sql.XAConnection; + +/** + * This is a utility class with JDBC helper functions. + */ +public class JdbcUtils { + + private static final String[] DRIVERS = { "h2:", "org.h2.Driver", "Cache:", + "com.intersys.jdbc.CacheDriver", "daffodilDB://", "in.co.daffodil.db.rmi.RmiDaffodilDBDriver", + "daffodil", "in.co.daffodil.db.jdbc.DaffodilDBDriver", "db2:", "COM.ibm.db2.jdbc.net.DB2Driver", + "derby:net:", "org.apache.derby.jdbc.ClientDriver", "derby://", + "org.apache.derby.jdbc.ClientDriver", "derby:", "org.apache.derby.jdbc.EmbeddedDriver", + "FrontBase:", "com.frontbase.jdbc.FBJDriver", "firebirdsql:", "org.firebirdsql.jdbc.FBDriver", + "hsqldb:", "org.hsqldb.jdbcDriver", "informix-sqli:", "com.informix.jdbc.IfxDriver", "jtds:", + "net.sourceforge.jtds.jdbc.Driver", "microsoft:", "com.microsoft.jdbc.sqlserver.SQLServerDriver", + "mimer:", "com.mimer.jdbc.Driver", "mysql:", "com.mysql.jdbc.Driver", "odbc:", + "sun.jdbc.odbc.JdbcOdbcDriver", "oracle:", "oracle.jdbc.driver.OracleDriver", "pervasive:", + "com.pervasive.jdbc.v2.Driver", "pointbase:micro:", "com.pointbase.me.jdbc.jdbcDriver", + "pointbase:", "com.pointbase.jdbc.jdbcUniversalDriver", "postgresql:", "org.postgresql.Driver", + "sybase:", "com.sybase.jdbc3.jdbc.SybDriver", "sqlserver:", + "com.microsoft.sqlserver.jdbc.SQLServerDriver", "teradata:", "com.ncr.teradata.TeraDriver", }; + + private JdbcUtils() { + // utility class + } + + /** + * Close a statement without throwing an exception. + * + * @param stat + * the statement or null + */ + public static void closeSilently(Statement stat) { + if (stat != null) { + try { + stat.close(); + } catch (SQLException e) { + // ignore + } + } + } + + /** + * Close a connection without throwing an exception. + * + * @param conn + * the connection or null + */ + public static void closeSilently(Connection conn) { + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + // ignore + } + } + } + + /** + * Close a result set without throwing an exception. + * + * @param rs + * the result set or null + */ + public static void closeSilently(ResultSet rs) { + closeSilently(rs, false); + } + + /** + * Close a result set, and optionally its statement without throwing an + * exception. + * + * @param rs + * the result set or null + */ + public static void closeSilently(ResultSet rs, boolean closeStatement) { + if (rs != null) { + Statement stat = null; + if (closeStatement) { + try { + stat = rs.getStatement(); + } catch (SQLException e) { + // ignore + } + } + try { + rs.close(); + } catch (SQLException e) { + // ignore + } + closeSilently(stat); + } + } + + /** + * Close an XA connection set without throwing an exception. + * + * @param conn + * the XA connection or null + */ + public static void closeSilently(XAConnection conn) { + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + // ignore + } + } + } + + /** + * Open a new database connection with the given settings. + * + * @param driver + * the driver class name + * @param url + * the database URL + * @param user + * the user name + * @param password + * the password + * @return the database connection + */ + public static Connection getConnection(String driver, String url, String user, String password) + throws SQLException { + Properties prop = new Properties(); + if (user != null) { + prop.setProperty("user", user); + } + if (password != null) { + prop.setProperty("password", password); + } + return getConnection(driver, url, prop); + } + + /** + * Escape table or schema patterns used for DatabaseMetaData functions. + * + * @param pattern + * the pattern + * @return the escaped pattern + */ + public static String escapeMetaDataPattern(String pattern) { + if (pattern == null || pattern.length() == 0) { + return pattern; + } + return StringUtils.replaceAll(pattern, "\\", "\\\\"); + } + + /** + * Open a new database connection with the given settings. + * + * @param driver + * the driver class name + * @param url + * the database URL + * @param prop + * the properties containing at least the user name and password + * @return the database connection + */ + public static Connection getConnection(String driver, String url, Properties prop) throws SQLException { + if (StringUtils.isNullOrEmpty(driver)) { + JdbcUtils.load(url); + } else { + Class d = Utils.loadClass(driver); + if (java.sql.Driver.class.isAssignableFrom(d)) { + return DriverManager.getConnection(url, prop); + } else if (javax.naming.Context.class.isAssignableFrom(d)) { + // JNDI context + try { + Context context = (Context) d.newInstance(); + DataSource ds = (DataSource) context.lookup(url); + String user = prop.getProperty("user"); + String password = prop.getProperty("password"); + if (StringUtils.isNullOrEmpty(user) && StringUtils.isNullOrEmpty(password)) { + return ds.getConnection(); + } + return ds.getConnection(user, password); + } catch (SQLException e) { + throw e; + } catch (Exception e) { + throw new SQLException("Failed to get connection for " + url, e); + } + } else { + // Don't know, but maybe it loaded a JDBC Driver + return DriverManager.getConnection(url, prop); + } + } + return DriverManager.getConnection(url, prop); + } + + /** + * Get the driver class name for the given URL, or null if the URL is + * unknown. + * + * @param url + * the database URL + * @return the driver class name + */ + public static String getDriver(String url) { + if (url.startsWith("jdbc:")) { + url = url.substring("jdbc:".length()); + for (int i = 0; i < DRIVERS.length; i += 2) { + String prefix = DRIVERS[i]; + if (url.startsWith(prefix)) { + return DRIVERS[i + 1]; + } + } + } + return null; + } + + /** + * Load the driver class for the given URL, if the database URL is known. + * + * @param url + * the database URL + */ + public static void load(String url) { + String driver = getDriver(url); + if (driver != null) { + Utils.loadClass(driver); + } + } + +} diff --git a/src/main/java/com/iciql/util/Slf4jIciqlListener.java b/src/main/java/com/iciql/util/Slf4jIciqlListener.java new file mode 100644 index 0000000..ded393f --- /dev/null +++ b/src/main/java/com/iciql/util/Slf4jIciqlListener.java @@ -0,0 +1,92 @@ +/* + * 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.util; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.iciql.Iciql; +import com.iciql.util.IciqlLogger.IciqlListener; +import com.iciql.util.IciqlLogger.StatementType; + +/** + * Slf4jIciqlListener interfaces the IciqlLogger to the SLF4J logging framework. + */ +public class Slf4jIciqlListener implements IciqlListener { + + private Logger logger = LoggerFactory.getLogger(Iciql.class); + + /** + * Enumeration representing the SLF4J log levels. + */ + public enum Level { + ERROR, WARN, INFO, DEBUG, TRACE, OFF; + } + + private final Level defaultLevel; + + private final Map levels; + + public Slf4jIciqlListener() { + this(Level.TRACE); + } + + public Slf4jIciqlListener(Level defaultLevel) { + this.defaultLevel = defaultLevel; + levels = new HashMap(); + for (StatementType type : StatementType.values()) { + levels.put(type, defaultLevel); + } + } + + /** + * Sets the logging level for a particular statement type. + * + * @param type + * @param level + */ + public void setLevel(StatementType type, Level level) { + levels.put(type, defaultLevel); + } + + @Override + public void logIciql(StatementType type, String statement) { + Level level = levels.get(type); + switch (level) { + case ERROR: + logger.error(statement); + break; + case WARN: + logger.warn(statement); + break; + case INFO: + logger.info(statement); + break; + case DEBUG: + logger.debug(statement); + break; + case TRACE: + logger.trace(statement); + break; + case OFF: + break; + } + } +} diff --git a/src/main/java/com/iciql/util/StatementBuilder.java b/src/main/java/com/iciql/util/StatementBuilder.java new file mode 100644 index 0000000..47e8054 --- /dev/null +++ b/src/main/java/com/iciql/util/StatementBuilder.java @@ -0,0 +1,166 @@ +/* + * 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.util; + +/** + * A utility class to build a statement. In addition to the methods supported by + * StringBuilder, it allows to add a text only in the second iteration. This + * simplified constructs such as: + * + *

+ * StringBuilder buff = new StringBuilder();
+ * for (int i = 0; i < args.length; i++) {
+ * 	if (i > 0) {
+ * 		buff.append(", ");
+ * 	}
+ * 	buff.append(args[i]);
+ * }
+ * 
+ * + * to + * + *
+ * StatementBuilder buff = new StatementBuilder();
+ * for (String s : args) {
+ * 	buff.appendExceptFirst(", ");
+ * 	buff.append(a);
+ * }
+ * 
+ */ +public class StatementBuilder { + + private final StringBuilder builder = new StringBuilder(); + private int index; + + /** + * Create a new builder. + */ + public StatementBuilder() { + // nothing to do + } + + /** + * Create a new builder. + * + * @param string + * the initial string + */ + public StatementBuilder(String string) { + builder.append(string); + } + + /** + * Append a text. + * + * @param s + * the text to append + * @return itself + */ + public StatementBuilder append(String s) { + builder.append(s); + return this; + } + + /** + * Append a character. + * + * @param c + * the character to append + * @return itself + */ + public StatementBuilder append(char c) { + builder.append(c); + return this; + } + + /** + * Append a number. + * + * @param x + * the number to append + * @return itself + */ + public StatementBuilder append(long x) { + builder.append(x); + return this; + } + + /** + * Returns the current value of the loop counter. + * + * @return the loop counter + */ + public int getCount() { + return index; + } + + /** + * Reset the loop counter. + * + * @return itself + */ + public StatementBuilder resetCount() { + index = 0; + return this; + } + + /** + * Append a text, but only if appendExceptFirst was never called. + * + * @param s + * the text to append + */ + public void appendOnlyFirst(String s) { + if (index == 0) { + builder.append(s); + } + } + + /** + * Append a text, except when this method is called the first time. + * + * @param s + * the text to append + */ + public void appendExceptFirst(String s) { + if (index++ > 0) { + builder.append(s); + } + } + + public void append(StatementBuilder sb) { + builder.append(sb); + } + + public void insert(int offset, char c) { + builder.insert(offset, c); + } + + public String toString() { + return builder.toString(); + } + + /** + * Get the length. + * + * @return the length + */ + public int length() { + return builder.length(); + } +} diff --git a/src/main/java/com/iciql/util/StringUtils.java b/src/main/java/com/iciql/util/StringUtils.java new file mode 100644 index 0000000..dd3f180 --- /dev/null +++ b/src/main/java/com/iciql/util/StringUtils.java @@ -0,0 +1,382 @@ +/* + * 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.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; + +/** + * Common string utilities. + * + */ +public class StringUtils { + + /** + * Replace all occurrences of the before string with the after string. + * + * @param s + * the string + * @param before + * the old text + * @param after + * the new text + * @return the string with the before string replaced + */ + public static String replaceAll(String s, String before, String after) { + int next = s.indexOf(before); + if (next < 0) { + return s; + } + StringBuilder buff = new StringBuilder(s.length() - before.length() + after.length()); + int index = 0; + while (true) { + buff.append(s.substring(index, next)).append(after); + index = next + before.length(); + next = s.indexOf(before, index); + if (next < 0) { + buff.append(s.substring(index)); + break; + } + } + return buff.toString(); + } + + /** + * Check if a String is null or empty (the length is null). + * + * @param s + * the string to check + * @return true if it is null or empty + */ + public static boolean isNullOrEmpty(String s) { + return s == null || s.length() == 0; + } + + /** + * Convert a string to a Java literal using the correct escape sequences. + * The literal is not enclosed in double quotes. The result can be used in + * properties files or in Java source code. + * + * @param s + * the text to convert + * @return the Java representation + */ + public static String javaEncode(String s) { + int length = s.length(); + StringBuilder buff = new StringBuilder(length); + for (int i = 0; i < length; i++) { + char c = s.charAt(i); + switch (c) { + // case '\b': + // // BS backspace + // // not supported in properties files + // buff.append("\\b"); + // break; + case '\t': + // HT horizontal tab + buff.append("\\t"); + break; + case '\n': + // LF linefeed + buff.append("\\n"); + break; + case '\f': + // FF form feed + buff.append("\\f"); + break; + case '\r': + // CR carriage return + buff.append("\\r"); + break; + case '"': + // double quote + buff.append("\\\""); + break; + case '\\': + // backslash + buff.append("\\\\"); + break; + default: + int ch = c & 0xffff; + if (ch >= ' ' && (ch < 0x80)) { + buff.append(c); + // not supported in properties files + // } else if(ch < 0xff) { + // buff.append("\\"); + // // make sure it's three characters (0x200 is octal 1000) + // buff.append(Integer.toOctalString(0x200 | + // ch).substring(1)); + } else { + buff.append("\\u"); + // make sure it's four characters + buff.append(Integer.toHexString(0x10000 | ch).substring(1)); + } + } + } + return buff.toString(); + } + + /** + * Pad a string. This method is used for the SQL function RPAD and LPAD. + * + * @param string + * the original string + * @param n + * the target length + * @param padding + * the padding string + * @param right + * true if the padding should be appended at the end + * @return the padded string + */ + public static String pad(String string, int n, String padding, boolean right) { + if (n < 0) { + n = 0; + } + if (n < string.length()) { + return string.substring(0, n); + } else if (n == string.length()) { + return string; + } + char paddingChar; + if (padding == null || padding.length() == 0) { + paddingChar = ' '; + } else { + paddingChar = padding.charAt(0); + } + StringBuilder buff = new StringBuilder(n); + n -= string.length(); + if (right) { + buff.append(string); + } + for (int i = 0; i < n; i++) { + buff.append(paddingChar); + } + if (!right) { + buff.append(string); + } + return buff.toString(); + } + + /** + * Convert a string to a SQL literal. Null is converted to NULL. The text is + * enclosed in single quotes. If there are any special characters, the + * method STRINGDECODE is used. + * + * @param s + * the text to convert. + * @return the SQL literal + */ + public static String quoteStringSQL(String s) { + if (s == null) { + return "NULL"; + } + int length = s.length(); + StringBuilder buff = new StringBuilder(length + 2); + buff.append('\''); + for (int i = 0; i < length; i++) { + char c = s.charAt(i); + if (c == '\'') { + buff.append(c); + } else if (c < ' ' || c > 127) { + // need to start from the beginning because maybe there was a \ + // that was not quoted + return "STRINGDECODE(" + quoteStringSQL(javaEncode(s)) + ")"; + } + buff.append(c); + } + buff.append('\''); + return buff.toString(); + } + + /** + * Split a string into an array of strings using the given separator. A null + * string will result in a null array, and an empty string in a zero element + * array. + * + * @param s + * the string to split + * @param separatorChar + * the separator character + * @param trim + * whether each element should be trimmed + * @return the array list + */ + public static String[] arraySplit(String s, char separatorChar, boolean trim) { + if (s == null) { + return null; + } + int length = s.length(); + if (length == 0) { + return new String[0]; + } + ArrayList list = Utils.newArrayList(); + StringBuilder buff = new StringBuilder(length); + for (int i = 0; i < length; i++) { + char c = s.charAt(i); + if (c == separatorChar) { + String e = buff.toString(); + list.add(trim ? e.trim() : e); + buff.setLength(0); + } else if (c == '\\' && i < length - 1) { + buff.append(s.charAt(++i)); + } else { + buff.append(c); + } + } + String e = buff.toString(); + list.add(trim ? e.trim() : e); + String[] array = new String[list.size()]; + list.toArray(array); + return array; + } + + /** + * Calculates the SHA1 of the string. + * + * @param text + * @return sha1 of the string + */ + public static String calculateSHA1(String text) { + try { + byte[] bytes = text.getBytes("iso-8859-1"); + return calculateSHA1(bytes); + } catch (UnsupportedEncodingException u) { + throw new RuntimeException(u); + } + } + + /** + * Calculates the SHA1 of the byte array. + * + * @param bytes + * @return sha1 of the byte array + */ + public static String calculateSHA1(byte[] bytes) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + md.update(bytes, 0, bytes.length); + byte[] digest = md.digest(); + StringBuilder sb = new StringBuilder(digest.length * 2); + for (int i = 0; i < digest.length; i++) { + if (((int) digest[i] & 0xff) < 0x10) { + sb.append('0'); + } + sb.append(Integer.toHexString((int) digest[i] & 0xff)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException t) { + throw new RuntimeException(t); + } + } + + /** + * Counts the occurrences of char c in the given string. + * + * @param c + * the character to count + * @param value + * the source string + * @return the count of c in value + */ + public static int count(char c, String value) { + int count = 0; + for (char cv : value.toCharArray()) { + if (cv == c) { + count++; + } + } + return count; + } + + /** + * Prepare text for html presentation. Replace sensitive characters with + * html entities. + * + * @param inStr + * @param changeSpace + * @return plain text escaped for html + */ + public static String escapeForHtml(String inStr, boolean changeSpace) { + StringBuffer retStr = new StringBuffer(); + int i = 0; + while (i < inStr.length()) { + if (inStr.charAt(i) == '&') { + retStr.append("&"); + } else if (inStr.charAt(i) == '<') { + retStr.append("<"); + } else if (inStr.charAt(i) == '>') { + retStr.append(">"); + } else if (inStr.charAt(i) == '\"') { + retStr.append("""); + } else if (changeSpace && inStr.charAt(i) == ' ') { + retStr.append(" "); + } else if (changeSpace && inStr.charAt(i) == '\t') { + retStr.append("    "); + } else { + retStr.append(inStr.charAt(i)); + } + i++; + } + return retStr.toString(); + } + + /** + * Replaces carriage returns and line feeds with html line breaks. + * + * @param string + * @return plain text with html line breaks + */ + public static String breakLinesForHtml(String string) { + return string.replace("\r\n", "
").replace("\r", "
").replace("\n", "
"); + } + + /** + * Returns the string content of the specified file. + * + * @param file + * @param lineEnding + * @return the string content of the file + */ + public static String readContent(File file, String lineEnding) { + StringBuilder sb = new StringBuilder(); + try { + InputStreamReader is = new InputStreamReader(new FileInputStream(file), Charset.forName("UTF-8")); + BufferedReader reader = new BufferedReader(is); + String line = null; + while ((line = reader.readLine()) != null) { + sb.append(line); + if (lineEnding != null) { + sb.append(lineEnding); + } + } + reader.close(); + } catch (Throwable t) { + System.err.println("Failed to read content of " + file.getAbsolutePath()); + t.printStackTrace(); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/iciql/util/Utils.java b/src/main/java/com/iciql/util/Utils.java new file mode 100644 index 0000000..77110b8 --- /dev/null +++ b/src/main/java/com/iciql/util/Utils.java @@ -0,0 +1,459 @@ +/* + * 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.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringWriter; +import java.lang.reflect.Constructor; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Blob; +import java.sql.Clob; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import com.iciql.Iciql.EnumId; +import com.iciql.Iciql.EnumType; +import com.iciql.IciqlException; + +/** + * Generic utility methods. + */ +public class Utils { + + public static final AtomicLong COUNTER = new AtomicLong(0); + + public static final AtomicInteger AS_COUNTER = new AtomicInteger(0); + + private static final boolean MAKE_ACCESSIBLE = true; + + private static final int BUFFER_BLOCK_SIZE = 4 * 1024; + + public static synchronized int nextAsCount() { + // prevent negative values and use a threadsafe counter + int count = AS_COUNTER.incrementAndGet(); + if (count == Integer.MAX_VALUE) { + count = 0; + AS_COUNTER.set(count); + } + return count; + } + + @SuppressWarnings("unchecked") + public static Class getClass(X x) { + return (Class) x.getClass(); + } + + public static Class loadClass(String className) { + try { + return Class.forName(className); + } catch (Exception e) { + throw new IciqlException(e); + } + } + + public static ArrayList newArrayList() { + return new ArrayList(); + } + + public static ArrayList newArrayList(Collection c) { + return new ArrayList(c); + } + + public static HashSet newHashSet() { + return new HashSet(); + } + + public static HashSet newHashSet(Collection list) { + return new HashSet(list); + } + + public static HashMap newHashMap() { + return new HashMap(); + } + + public static Map newSynchronizedHashMap() { + HashMap map = newHashMap(); + return Collections.synchronizedMap(map); + } + + public static IdentityHashMap newIdentityHashMap() { + return new IdentityHashMap(); + } + + public static ThreadLocal newThreadLocal(final Class clazz) { + return new ThreadLocal() { + @SuppressWarnings("rawtypes") + @Override + protected T initialValue() { + try { + return clazz.newInstance(); + } catch (Exception e) { + if (MAKE_ACCESSIBLE) { + Constructor[] constructors = clazz.getDeclaredConstructors(); + // try 0 length constructors + for (Constructor c : constructors) { + if (c.getParameterTypes().length == 0) { + c.setAccessible(true); + try { + return clazz.newInstance(); + } catch (Exception e2) { + // ignore + } + } + } + } + throw new IciqlException(e, + "Missing default constructor? Exception trying to instantiate {0}: {1}", + clazz.getName(), e.getMessage()); + } + } + }; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static T newObject(Class clazz) { + // must create new instances + if (clazz == int.class || clazz == Integer.class) { + return (T) new Integer((int) COUNTER.getAndIncrement()); + } else if (clazz == String.class) { + return (T) ("" + COUNTER.getAndIncrement()); + } else if (clazz == long.class || clazz == Long.class) { + return (T) new Long(COUNTER.getAndIncrement()); + } else if (clazz == short.class || clazz == Short.class) { + return (T) new Short((short) COUNTER.getAndIncrement()); + } else if (clazz == byte.class || clazz == Byte.class) { + return (T) new Byte((byte) COUNTER.getAndIncrement()); + } else if (clazz == float.class || clazz == Float.class) { + return (T) new Float(COUNTER.getAndIncrement()); + } else if (clazz == double.class || clazz == Double.class) { + return (T) new Double(COUNTER.getAndIncrement()); + } else if (clazz == boolean.class || clazz == Boolean.class) { + COUNTER.getAndIncrement(); + return (T) new Boolean(false); + } else if (clazz == BigDecimal.class) { + return (T) new BigDecimal(COUNTER.getAndIncrement()); + } else if (clazz == BigInteger.class) { + return (T) new BigInteger("" + COUNTER.getAndIncrement()); + } else if (clazz == java.sql.Date.class) { + return (T) new java.sql.Date(COUNTER.getAndIncrement()); + } else if (clazz == java.sql.Time.class) { + return (T) new java.sql.Time(COUNTER.getAndIncrement()); + } else if (clazz == java.sql.Timestamp.class) { + return (T) new java.sql.Timestamp(COUNTER.getAndIncrement()); + } else if (clazz == java.util.Date.class) { + return (T) new java.util.Date(COUNTER.getAndIncrement()); + } else if (clazz == byte[].class) { + COUNTER.getAndIncrement(); + return (T) new byte[0]; + } else if (clazz.isEnum()) { + COUNTER.getAndIncrement(); + // enums can not be instantiated reflectively + // return first constant as reference + return clazz.getEnumConstants()[0]; + } else if (clazz == java.util.UUID.class) { + COUNTER.getAndIncrement(); + return (T) UUID.randomUUID(); + } + try { + return clazz.newInstance(); + } catch (Exception e) { + if (MAKE_ACCESSIBLE) { + Constructor[] constructors = clazz.getDeclaredConstructors(); + // try 0 length constructors + for (Constructor c : constructors) { + if (c.getParameterTypes().length == 0) { + c.setAccessible(true); + try { + return clazz.newInstance(); + } catch (Exception e2) { + // ignore + } + } + } + // try 1 length constructors + for (Constructor c : constructors) { + if (c.getParameterTypes().length == 1) { + c.setAccessible(true); + try { + return (T) c.newInstance(new Object[1]); + } catch (Exception e2) { + // ignore + } + } + } + } + throw new IciqlException(e, + "Missing default constructor?! Exception trying to instantiate {0}: {1}", + clazz.getName(), e.getMessage()); + } + } + + public static boolean isSimpleType(Class clazz) { + if (Number.class.isAssignableFrom(clazz)) { + return true; + } else if (clazz == String.class) { + return true; + } + return false; + } + + public static Object convert(Object o, Class targetType) { + if (o == null) { + return null; + } + Class currentType = o.getClass(); + if (targetType.isAssignableFrom(currentType)) { + return o; + } + + // convert from CLOB/TEXT/VARCHAR to String + if (targetType == String.class) { + if (Clob.class.isAssignableFrom(currentType)) { + Clob c = (Clob) o; + try { + Reader r = c.getCharacterStream(); + return readStringAndClose(r, -1); + } catch (Exception e) { + throw new IciqlException(e, "error converting CLOB to String: ", e.toString()); + } + } + return o.toString(); + } + + if (Boolean.class.isAssignableFrom(targetType) || boolean.class.isAssignableFrom(targetType)) { + // convert from number to boolean + if (Number.class.isAssignableFrom(currentType)) { + Number n = (Number) o; + return n.intValue() > 0; + } + // convert from string to boolean + if (String.class.isAssignableFrom(currentType)) { + String s = o.toString().toLowerCase(); + float f = 0f; + try { + f = Float.parseFloat(s); + } catch (Exception e) { + } + return f > 0 || s.equals("true") || s.equals("yes") || s.equals("y") || s.equals("on"); + } + } + + // convert from boolean to number + if (Boolean.class.isAssignableFrom(currentType)) { + Boolean b = (Boolean) o; + if (Number.class.isAssignableFrom(targetType)) { + return b ? 1 : 0; + } + if (boolean.class.isAssignableFrom(targetType)) { + return b.booleanValue(); + } + } + + // convert from number to number + if (Number.class.isAssignableFrom(currentType)) { + Number n = (Number) o; + if (targetType == byte.class || targetType == Byte.class) { + return n.byteValue(); + } else if (targetType == short.class || targetType == Short.class) { + return n.shortValue(); + } else if (targetType == int.class || targetType == Integer.class) { + return n.intValue(); + } else if (targetType == long.class || targetType == Long.class) { + return n.longValue(); + } else if (targetType == double.class || targetType == Double.class) { + return n.doubleValue(); + } else if (targetType == float.class || targetType == Float.class) { + return n.floatValue(); + } + } + + // convert from BLOB + if (targetType == byte[].class) { + if (Blob.class.isAssignableFrom(currentType)) { + Blob b = (Blob) o; + try { + InputStream is = b.getBinaryStream(); + return readBlobAndClose(is, -1); + } catch (Exception e) { + throw new IciqlException(e, "error converting BLOB to byte[]: ", e.toString()); + } + } + } + throw new IciqlException("Can not convert the value {0} from {1} to {2}", o, currentType, targetType); + } + + public static Object convertEnum(Enum o, EnumType type) { + if (o == null) { + return null; + } + switch (type) { + case ORDINAL: + return o.ordinal(); + case ENUMID: + if (!EnumId.class.isAssignableFrom(o.getClass())) { + throw new IciqlException("Can not convert the enum {0} using ENUMID", o); + } + EnumId enumid = (EnumId) o; + return enumid.enumId(); + case NAME: + default: + return o.name(); + } + } + + public static Object convertEnum(Object o, Class targetType, EnumType type) { + if (o == null) { + return null; + } + Class currentType = o.getClass(); + if (targetType.isAssignableFrom(currentType)) { + return o; + } + // convert from VARCHAR/TEXT/INT to Enum + Enum[] values = (Enum[]) targetType.getEnumConstants(); + if (Clob.class.isAssignableFrom(currentType)) { + // TEXT/CLOB field + Clob c = (Clob) o; + String name = null; + try { + Reader r = c.getCharacterStream(); + name = readStringAndClose(r, -1); + } catch (Exception e) { + throw new IciqlException(e, "error converting CLOB to String: ", e.toString()); + } + + // find name match + for (Enum value : values) { + if (value.name().equalsIgnoreCase(name)) { + return value; + } + } + } else if (String.class.isAssignableFrom(currentType)) { + // VARCHAR field + String name = (String) o; + for (Enum value : values) { + if (value.name().equalsIgnoreCase(name)) { + return value; + } + } + } else if (Number.class.isAssignableFrom(currentType)) { + // INT field + int n = ((Number) o).intValue(); + if (type.equals(EnumType.ORDINAL)) { + // ORDINAL mapping + for (Enum value : values) { + if (value.ordinal() == n) { + return value; + } + } + } else if (type.equals(EnumType.ENUMID)) { + if (!EnumId.class.isAssignableFrom(targetType)) { + throw new IciqlException("Can not convert the value {0} from {1} to {2} using ENUMID", o, + currentType, targetType); + } + // ENUMID mapping + for (Enum value : values) { + EnumId enumid = (EnumId) value; + if (enumid.enumId() == n) { + return value; + } + } + } + } + throw new IciqlException("Can not convert the value {0} from {1} to {2}", o, currentType, targetType); + } + + /** + * Read a number of characters from a reader and close it. + * + * @param in + * the reader + * @param length + * the maximum number of characters to read, or -1 to read until + * the end of file + * @return the string read + */ + public static String readStringAndClose(Reader in, int length) throws IOException { + try { + if (length <= 0) { + length = Integer.MAX_VALUE; + } + int block = Math.min(BUFFER_BLOCK_SIZE, length); + StringWriter out = new StringWriter(length == Integer.MAX_VALUE ? block : length); + char[] buff = new char[block]; + while (length > 0) { + int len = Math.min(block, length); + len = in.read(buff, 0, len); + if (len < 0) { + break; + } + out.write(buff, 0, len); + length -= len; + } + return out.toString(); + } finally { + in.close(); + } + } + + /** + * Read a number of bytes from a stream and close it. + * + * @param in + * the stream + * @param length + * the maximum number of bytes to read, or -1 to read until the + * end of file + * @return the string read + */ + public static byte[] readBlobAndClose(InputStream in, int length) throws IOException { + try { + if (length <= 0) { + length = Integer.MAX_VALUE; + } + int block = Math.min(BUFFER_BLOCK_SIZE, length); + ByteArrayOutputStream out = new ByteArrayOutputStream(length == Integer.MAX_VALUE ? block + : length); + byte[] buff = new byte[block]; + while (length > 0) { + int len = Math.min(block, length); + len = in.read(buff, 0, len); + if (len < 0) { + break; + } + out.write(buff, 0, len); + length -= len; + } + return out.toByteArray(); + } finally { + in.close(); + } + } +} diff --git a/src/main/java/com/iciql/util/WeakIdentityHashMap.java b/src/main/java/com/iciql/util/WeakIdentityHashMap.java new file mode 100644 index 0000000..bc03cd0 --- /dev/null +++ b/src/main/java/com/iciql/util/WeakIdentityHashMap.java @@ -0,0 +1,243 @@ +/* + * 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.util; + +import java.lang.ref.WeakReference; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import com.iciql.IciqlException; + +/** + * This hash map uses weak references, so that elements that are no longer + * referenced elsewhere can be garbage collected. It also uses object identity + * to compare keys. The garbage collection happens when trying to add new data, + * or when resizing. + * + * @param + * the keys + * @param + * the value + */ + +public class WeakIdentityHashMap implements Map { + + private static final int MAX_LOAD = 90; + private static final WeakReference DELETED_KEY = new WeakReference(null); + private int mask, len, size, deletedCount, level; + private int maxSize, minSize, maxDeleted; + private WeakReference[] keys; + private V[] values; + + public WeakIdentityHashMap() { + reset(2); + } + + public int size() { + return size; + } + + private void checkSizePut() { + if (deletedCount > size) { + rehash(level); + } + if (size + deletedCount >= maxSize) { + rehash(level + 1); + } + } + + private void checkSizeRemove() { + if (size < minSize && level > 0) { + rehash(level - 1); + } else if (deletedCount > maxDeleted) { + rehash(level); + } + } + + private int getIndex(Object key) { + return System.identityHashCode(key) & mask; + } + + @SuppressWarnings("unchecked") + private void reset(int newLevel) { + minSize = size * 3 / 4; + size = 0; + level = newLevel; + len = 2 << level; + mask = len - 1; + maxSize = (int) (len * MAX_LOAD / 100L); + deletedCount = 0; + maxDeleted = 20 + len / 2; + keys = new WeakReference[len]; + values = (V[]) new Object[len]; + } + + public V put(K key, V value) { + checkSizePut(); + int index = getIndex(key); + int plus = 1; + int deleted = -1; + do { + WeakReference k = keys[index]; + if (k == null) { + // found an empty record + if (deleted >= 0) { + index = deleted; + deletedCount--; + } + size++; + keys[index] = new WeakReference(key); + values[index] = value; + return null; + } else if (k == DELETED_KEY) { + if (deleted < 0) { + // found the first deleted record + deleted = index; + } + } else { + Object r = k.get(); + if (r == null) { + delete(index); + } else if (r == key) { + // update existing + V old = values[index]; + values[index] = value; + return old; + } + } + index = (index + plus++) & mask; + } while (plus <= len); + throw new IciqlException("Hashmap is full"); + } + + public V remove(Object key) { + checkSizeRemove(); + int index = getIndex(key); + int plus = 1; + do { + WeakReference k = keys[index]; + if (k == null) { + // found an empty record + return null; + } else if (k == DELETED_KEY) { + // continue + } else { + Object r = k.get(); + if (r == null) { + delete(index); + } else if (r == key) { + // found the record + V old = values[index]; + delete(index); + return old; + } + } + index = (index + plus++) & mask; + k = keys[index]; + } while (plus <= len); + // not found + return null; + } + + @SuppressWarnings("unchecked") + private void delete(int index) { + keys[index] = (WeakReference) DELETED_KEY; + values[index] = null; + deletedCount++; + size--; + } + + private void rehash(int newLevel) { + WeakReference[] oldKeys = keys; + V[] oldValues = values; + reset(newLevel); + for (int i = 0; i < oldKeys.length; i++) { + WeakReference k = oldKeys[i]; + if (k != null && k != DELETED_KEY) { + K key = k.get(); + if (key != null) { + put(key, oldValues[i]); + } + } + } + } + + public V get(Object key) { + int index = getIndex(key); + int plus = 1; + do { + WeakReference k = keys[index]; + if (k == null) { + return null; + } else if (k == DELETED_KEY) { + // continue + } else { + Object r = k.get(); + if (r == null) { + delete(index); + } else if (r == key) { + return values[index]; + } + } + index = (index + plus++) & mask; + } while (plus <= len); + return null; + } + + public void clear() { + reset(2); + } + + public boolean containsKey(Object key) { + return get(key) != null; + } + + public boolean containsValue(Object value) { + if (value == null) { + return false; + } + for (V item : values) { + if (value.equals(item)) { + return true; + } + } + return false; + } + + public Set> entrySet() { + throw new UnsupportedOperationException(); + } + + public boolean isEmpty() { + return size == 0; + } + + public Set keySet() { + throw new UnsupportedOperationException(); + } + + public void putAll(Map m) { + throw new UnsupportedOperationException(); + } + + public Collection values() { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/com/iciql/util/package.html b/src/main/java/com/iciql/util/package.html new file mode 100644 index 0000000..3d24dee --- /dev/null +++ b/src/main/java/com/iciql/util/package.html @@ -0,0 +1,25 @@ + + + + +Javadoc package documentation + + +Utility classes for iciql. + + \ No newline at end of file -- cgit v1.2.3