diff options
Diffstat (limited to 'src/com/iciql')
59 files changed, 9591 insertions, 0 deletions
diff --git a/src/com/iciql/CompareType.java b/src/com/iciql/CompareType.java new file mode 100644 index 0000000..eb401bb --- /dev/null +++ b/src/com/iciql/CompareType.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;
+
+/**
+ * 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);
+
+ 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/com/iciql/Condition.java b/src/com/iciql/Condition.java new file mode 100644 index 0000000..df6b033 --- /dev/null +++ b/src/com/iciql/Condition.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;
+
+/**
+ * A condition contains one or two operands and a compare operation.
+ *
+ * @param <A>
+ * the operand type
+ */
+
+class Condition<A> implements Token {
+ CompareType compareType;
+ A x, y;
+
+ Condition(A x, A y, CompareType compareType) {
+ this.compareType = compareType;
+ this.x = x;
+ this.y = y;
+ }
+
+ public <T> void appendSQL(SQLStatement stat, Query<T> query) {
+ query.appendSQL(stat, x);
+ stat.appendSQL(" ");
+ stat.appendSQL(compareType.getString());
+ if (compareType.hasRightExpression()) {
+ stat.appendSQL(" ");
+ query.appendSQL(stat, y);
+ }
+ }
+}
diff --git a/src/com/iciql/ConditionAndOr.java b/src/com/iciql/ConditionAndOr.java new file mode 100644 index 0000000..4d1cd0e --- /dev/null +++ b/src/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 <T> void appendSQL(SQLStatement stat, Query<T> query) {
+ stat.appendSQL(text);
+ }
+
+}
diff --git a/src/com/iciql/Constants.java b/src/com/iciql/Constants.java new file mode 100644 index 0000000..6bbbc71 --- /dev/null +++ b/src/com/iciql/Constants.java @@ -0,0 +1,42 @@ +/*
+ * 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 = "0.5.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 = "1";
+
+ // 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_PREVIOUS = "1";
+
+}
diff --git a/src/com/iciql/Db.java b/src/com/iciql/Db.java new file mode 100644 index 0000000..3f86d15 --- /dev/null +++ b/src/com/iciql/Db.java @@ -0,0 +1,470 @@ +/*
+ * 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.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+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.Properties;
+import java.util.Set;
+
+import javax.sql.DataSource;
+
+import com.iciql.DbUpgrader.DefaultDbUpgrader;
+import com.iciql.Iciql.IQDatabase;
+import com.iciql.Iciql.IQTable;
+import com.iciql.SQLDialect.DefaultSQLDialect;
+import com.iciql.SQLDialect.H2Dialect;
+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<Object, Token> TOKENS;
+
+ private static final Map<String, Class<? extends SQLDialect>> DIALECTS;
+
+ private final Connection conn;
+ private final Map<Class<?>, TableDefinition<?>> classMap = Collections
+ .synchronizedMap(new HashMap<Class<?>, TableDefinition<?>>());
+ private final SQLDialect dialect;
+ private DbUpgrader dbUpgrader = new DefaultDbUpgrader();
+ private final Set<Class<?>> upgradeChecked = Collections.synchronizedSet(new HashSet<Class<?>>());
+
+ static {
+ TOKENS = Collections.synchronizedMap(new WeakIdentityHashMap<Object, Token>());
+ DIALECTS = Collections.synchronizedMap(new HashMap<String, Class<? extends SQLDialect>>());
+ DIALECTS.put("org.h2", H2Dialect.class);
+ }
+
+ private Db(Connection conn) {
+ this.conn = conn;
+ dialect = getDialect(conn.getClass().getCanonicalName());
+ dialect.configureDialect(conn);
+ }
+
+ public static void registerDialect(Connection conn, Class<? extends SQLDialect> dialectClass) {
+ registerDialect(conn.getClass().getCanonicalName(), dialectClass);
+ }
+
+ public static void registerDialect(String connClass, Class<? extends SQLDialect> dialectClass) {
+ DIALECTS.put(connClass, dialectClass);
+ }
+
+ SQLDialect getDialect(String clazz) {
+ // try dialect by connection class name
+ Class<? extends SQLDialect> dialectClass = DIALECTS.get(clazz);
+ int lastDot = 0;
+ while (dialectClass == null) {
+ // try dialect by package name
+ int nextDot = clazz.indexOf('.', lastDot);
+ if (nextDot > -1) {
+ String pkg = clazz.substring(0, nextDot);
+ lastDot = nextDot + 1;
+ dialectClass = DIALECTS.get(pkg);
+ } else {
+ dialectClass = DefaultSQLDialect.class;
+ }
+ }
+ return instance(dialectClass);
+ }
+
+ static <X> X registerToken(X x, Token token) {
+ TOKENS.put(x, token);
+ return x;
+ }
+
+ static Token getToken(Object x) {
+ return TOKENS.get(x);
+ }
+
+ private static <T> T instance(Class<T> clazz) {
+ try {
+ return clazz.newInstance();
+ } catch (Exception 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 convert(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 convert(e);
+ }
+ }
+
+ public static Db open(Connection conn) {
+ return new Db(conn);
+ }
+
+ public static Db open(String url, String user, char[] password) {
+ try {
+ Properties prop = new Properties();
+ prop.setProperty("user", user);
+ prop.put("password", password);
+ Connection conn = JdbcUtils.getConnection(null, url, prop);
+ return new Db(conn);
+ } catch (SQLException e) {
+ throw convert(e);
+ }
+ }
+
+ private static Error convert(Exception e) {
+ return new Error(e);
+ }
+
+ public <T> void insert(T t) {
+ Class<?> clazz = t.getClass();
+ define(clazz).createTableIfRequired(this).insert(this, t, false);
+ }
+
+ public <T> long insertAndGetKey(T t) {
+ Class<?> clazz = t.getClass();
+ return define(clazz).createTableIfRequired(this).insert(this, t, true);
+ }
+
+ /**
+ * Merge usually 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 dialect does not support merge an IciqlException will be thrown.
+ *
+ * @param t
+ */
+ public <T> void merge(T t) {
+ if (!getDialect().supportsMerge()) {
+ throw new IciqlException("Merge is not supported by this SQL dialect");
+ }
+ Class<?> clazz = t.getClass();
+ define(clazz).createTableIfRequired(this).merge(this, t);
+ }
+
+ public <T> void update(T t) {
+ Class<?> clazz = t.getClass();
+ define(clazz).createTableIfRequired(this).update(this, t);
+ }
+
+ public <T> void delete(T t) {
+ Class<?> clazz = t.getClass();
+ define(clazz).createTableIfRequired(this).delete(this, t);
+ }
+
+ public <T extends Object> Query<T> from(T alias) {
+ Class<?> clazz = alias.getClass();
+ define(clazz).createTableIfRequired(this);
+ return Query.from(this, alias);
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T> List<T> buildObjects(Class<? extends T> modelClass, ResultSet rs) {
+ List<T> result = new ArrayList<T>();
+ TableDefinition<T> def = (TableDefinition<T>) define(modelClass).createTableIfRequired(this);
+ try {
+ while (rs.next()) {
+ T item = Utils.newObject(modelClass);
+ def.readRow(item, rs);
+ 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());
+
+ IQDatabase model = dbUpgrader.getClass().getAnnotation(IQDatabase.class);
+ if (model.version() > 0) {
+ DbVersion v = new DbVersion();
+ DbVersion dbVersion =
+ // (SCHEMA="" && TABLE="") == DATABASE
+ from(v).where(v.schema).is("").and(v.table).is("").selectFirst();
+ if (dbVersion == null) {
+ // database has no version registration, but model specifies
+ // version: insert DbVersion entry and return.
+ DbVersion newDb = new DbVersion(model.version());
+ insert(newDb);
+ } else {
+ // database has a version registration:
+ // check to see if upgrade is required.
+ if ((model.version() > dbVersion.version) && (dbUpgrader != null)) {
+ // database is an older version than the model
+ boolean success = dbUpgrader
+ .upgradeDatabase(this, dbVersion.version, model.version());
+ if (success) {
+ dbVersion.version = model.version();
+ update(dbVersion);
+ }
+ }
+ }
+ }
+ }
+ return this;
+ }
+
+ <T> void upgradeTable(TableDefinition<T> 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.schema).like(schema).and(v.table).like(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.schema = schema;
+ newTable.table = 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);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ <T> TableDefinition<T> define(Class<T> clazz) {
+ TableDefinition<T> def = getTableDefinition(clazz);
+ if (def == null) {
+ upgradeDb();
+ def = new TableDefinition<T>(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);
+ }
+ }
+ return def;
+ }
+
+ public synchronized void setDbUpgrader(DbUpgrader upgrader) {
+ if (!upgrader.getClass().isAnnotationPresent(IQDatabase.class)) {
+ throw new IciqlException("DbUpgrader must be annotated with " + IQDatabase.class.getSimpleName());
+ }
+ this.dbUpgrader = upgrader;
+ upgradeChecked.clear();
+ }
+
+ SQLDialect getDialect() {
+ return dialect;
+ }
+
+ public Connection getConnection() {
+ return conn;
+ }
+
+ public void close() {
+ try {
+ conn.close();
+ } catch (Exception e) {
+ throw new IciqlException(e);
+ }
+ }
+
+ public <A> TestCondition<A> test(A x) {
+ return new TestCondition<A>(x);
+ }
+
+ public <T> void insertAll(List<T> list) {
+ for (T t : list) {
+ insert(t);
+ }
+ }
+
+ public <T> List<Long> insertAllAndGetKeys(List<T> list) {
+ List<Long> identities = new ArrayList<Long>();
+ for (T t : list) {
+ identities.add(insertAndGetKey(t));
+ }
+ return identities;
+ }
+
+ public <T> void updateAll(List<T> list) {
+ for (T t : list) {
+ update(t);
+ }
+ }
+
+ public <T> void deleteAll(List<T> list) {
+ for (T t : list) {
+ delete(t);
+ }
+ }
+
+ PreparedStatement prepare(String sql, boolean returnGeneratedKeys) {
+ try {
+ if (returnGeneratedKeys) {
+ return conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
+ }
+ return conn.prepareStatement(sql);
+ } catch (SQLException e) {
+ throw new IciqlException(e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ <T> TableDefinition<T> getTableDefinition(Class<T> clazz) {
+ return (TableDefinition<T>) classMap.get(clazz);
+ }
+
+ /**
+ * Run a SQL query directly against the database.
+ *
+ * Be sure to close the ResultSet with
+ *
+ * <pre>
+ * JdbcUtils.closeSilently(rs, true);
+ * </pre>
+ *
+ * @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 <T> List<T> executeQuery(Class<? extends T> 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();
+ }
+ return buildObjects(modelClass, 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) {
+ Statement stat = null;
+ try {
+ stat = conn.createStatement();
+ int updateCount = stat.executeUpdate(sql);
+ return updateCount;
+ } catch (SQLException e) {
+ throw new IciqlException(e);
+ } finally {
+ JdbcUtils.closeSilently(stat);
+ }
+ }
+}
diff --git a/src/com/iciql/DbInspector.java b/src/com/iciql/DbInspector.java new file mode 100644 index 0000000..fb1e5d6 --- /dev/null +++ b/src/com/iciql/DbInspector.java @@ -0,0 +1,196 @@ +/* + * 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<? extends java.util.Date> dateTimeClass = java.util.Date.class; + + public DbInspector(Db db) { + this.db = db; + } + + /** + * 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<? extends java.util.Date> 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<String> generateModel(String schema, String table, String packageName, boolean annotateSchema, + boolean trimStrings) { + try { + List<String> models = Utils.newArrayList(); + List<TableInspector> 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 <T> List<ValidationRemark> validateModel(T model, boolean throwOnError) { + try { + TableInspector inspector = getTable(model); + inspector.read(metaData); + @SuppressWarnings("unchecked") + Class<T> clazz = (Class<T>) model.getClass(); + TableDefinition<T> 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 <T> TableInspector getTable(T model) throws SQLException { + @SuppressWarnings("unchecked") + Class<T> clazz = (Class<T>) model.getClass(); + TableDefinition<T> 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<TableInspector> 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<TableInspector> getTables(String schema, String table) throws SQLException { + ResultSet rs = null; + try { + rs = getMetaData().getSchemas(); + ArrayList<String> schemaList = Utils.newArrayList(); + while (rs.next()) { + schemaList.add(rs.getString("TABLE_SCHEM")); + } + JdbcUtils.closeSilently(rs); + + String iciqlTables = DbVersion.class.getAnnotation(IQTable.class).name(); + + List<TableInspector> 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.equalsIgnoreCase(iciqlTables)) { + tables.add(new TableInspector(s, t, getMetaData().storesUpperCaseIdentifiers(), dateTimeClass)); + } + } + } + + if (StringUtils.isNullOrEmpty(schema) && StringUtils.isNullOrEmpty(table)) { + // all schemas and tables + return tables; + } + // schema subset OR table subset OR exact match + List<TableInspector> 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/com/iciql/DbUpgrader.java b/src/com/iciql/DbUpgrader.java new file mode 100644 index 0000000..c4ab36b --- /dev/null +++ b/src/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.IQDatabase; + +/** + * Interface which defines a class to handle table changes based on model + * versions. An implementation of <i>DbUpgrader</i> must be annotated with the + * <i>IQDatabase</i> 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 <i>DbUpgrader</i> 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. + */ + @IQDatabase(version = 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/com/iciql/DbVersion.java b/src/com/iciql/DbVersion.java new file mode 100644 index 0000000..50d24a7 --- /dev/null +++ b/src/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(name = "schemaName", allowNull = false) + String schema = ""; + + @IQColumn(name = "tableName", allowNull = false) + String table = ""; + + @IQColumn(name = "version") + 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.schema = ""; + this.table = ""; + this.version = version; + } + +} diff --git a/src/com/iciql/Define.java b/src/com/iciql/Define.java new file mode 100644 index 0000000..54e435f --- /dev/null +++ b/src/com/iciql/Define.java @@ -0,0 +1,88 @@ +/*
+ * 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.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 primaryKey(Object... columns) {
+ checkInDefine();
+ currentTableDefinition.setPrimaryKey(columns);
+ }
+
+ public static void index(Object... columns) {
+ checkInDefine();
+ currentTableDefinition.addIndex(IndexType.STANDARD, columns);
+ }
+
+ public static void uniqueIndex(Object... columns) {
+ checkInDefine();
+ currentTableDefinition.addIndex(IndexType.UNIQUE, columns);
+ }
+
+ public static void hashIndex(Object column) {
+ checkInDefine();
+ currentTableDefinition.addIndex(IndexType.HASH, new Object[] { column });
+ }
+
+ public static void uniqueHashIndex(Object column) {
+ checkInDefine();
+ currentTableDefinition.addIndex(IndexType.UNIQUE_HASH, new Object[] { column });
+ }
+
+ public static void columnName(Object column, String columnName) {
+ checkInDefine();
+ currentTableDefinition.setColumnName(column, columnName);
+ }
+
+ public static void maxLength(Object column, int length) {
+ checkInDefine();
+ currentTableDefinition.setMaxLength(column, length);
+ }
+
+ public static void tableName(String tableName) {
+ checkInDefine();
+ currentTableDefinition.setTableName(tableName);
+ }
+
+ static synchronized <T> void define(TableDefinition<T> 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/com/iciql/Filter.java b/src/com/iciql/Filter.java new file mode 100644 index 0000000..99dbdc3 --- /dev/null +++ b/src/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/com/iciql/Function.java b/src/com/iciql/Function.java new file mode 100644 index 0000000..f2c9e30 --- /dev/null +++ b/src/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 = Long.valueOf(0);
+
+ protected Object[] x;
+ private String name;
+
+ protected Function(String name, Object... x) {
+ this.name = name;
+ this.x = x;
+ }
+
+ public <T> void appendSQL(SQLStatement stat, Query<T> query) {
+ stat.appendSQL(name).appendSQL("(");
+ int i = 0;
+ for (Object o : x) {
+ if (i++ > 0) {
+ stat.appendSQL(",");
+ }
+ query.appendSQL(stat, 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 extends Number> 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 <T> void appendSQL(SQLStatement stat, Query<T> query) {
+ query.appendSQL(stat, x[0]);
+ stat.appendSQL(" IS NULL");
+ }
+ });
+ }
+
+ public static Boolean isNotNull(Object x) {
+ return Db.registerToken(Utils.newObject(Boolean.class), new Function("", x) {
+ public <T> void appendSQL(SQLStatement stat, Query<T> query) {
+ query.appendSQL(stat, x[0]);
+ stat.appendSQL(" IS NOT NULL");
+ }
+ });
+ }
+
+ public static Boolean not(Boolean x) {
+ return Db.registerToken(Utils.newObject(Boolean.class), new Function("", x) {
+ public <T> void appendSQL(SQLStatement stat, Query<T> query) {
+ stat.appendSQL("NOT ");
+ query.appendSQL(stat, x[0]);
+ }
+ });
+ }
+
+ public static Boolean or(Boolean... x) {
+ return Db.registerToken(Utils.newObject(Boolean.class), new Function("", (Object[]) x) {
+ public <T> void appendSQL(SQLStatement stat, Query<T> query) {
+ int i = 0;
+ for (Object o : x) {
+ if (i++ > 0) {
+ stat.appendSQL(" OR ");
+ }
+ query.appendSQL(stat, o);
+ }
+ }
+ });
+ }
+
+ public static Boolean and(Boolean... x) {
+ return Db.registerToken(Utils.newObject(Boolean.class), new Function("", (Object[]) x) {
+ public <T> void appendSQL(SQLStatement stat, Query<T> query) {
+ int i = 0;
+ for (Object o : x) {
+ if (i++ > 0) {
+ stat.appendSQL(" AND ");
+ }
+ query.appendSQL(stat, o);
+ }
+ }
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <X> X min(X x) {
+ Class<X> clazz = (Class<X>) x.getClass();
+ X o = Utils.newObject(clazz);
+ return Db.registerToken(o, new Function("MIN", x));
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <X> X max(X x) {
+ Class<X> clazz = (Class<X>) 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 <T> void appendSQL(SQLStatement stat, Query<T> query) {
+ stat.appendSQL("(");
+ query.appendSQL(stat, x[0]);
+ stat.appendSQL(" LIKE ");
+ query.appendSQL(stat, x[1]);
+ stat.appendSQL(")");
+ }
+ });
+ }
+
+}
diff --git a/src/com/iciql/Iciql.java b/src/com/iciql/Iciql.java new file mode 100644 index 0000000..7267d33 --- /dev/null +++ b/src/com/iciql/Iciql.java @@ -0,0 +1,383 @@ +/*
+ * 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.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.
+ * <p>
+ * 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).
+ * <p>
+ * If a class is annotated with IQTable and at the same time implements Table,
+ * the define() method is not called.
+ * <p>
+ * Supported data types:
+ * <table>
+ * <tr>
+ * <td>java.lang.String</td>
+ * <td>VARCHAR (maxLength > 0) or TEXT (maxLength == 0)</td>
+ * </tr>
+ * <tr>
+ * <td>java.lang.Boolean</td>
+ * <td>BIT</td>
+ * </tr>
+ * <tr>
+ * <td>java.lang.Byte</td>
+ * <td>TINYINT</td>
+ * </tr>
+ * <tr>
+ * <td>java.lang.Short</td>
+ * <td>SMALLINT</td>
+ * </tr>
+ * <tr>
+ * <td>java.lang.Integer</td>
+ * <td>INT</td>
+ * </tr>
+ * <tr>
+ * <td>java.lang.Long</td>
+ * <td>BIGINT</td>
+ * </tr>
+ * <tr>
+ * <td>java.lang.Float</td>
+ * <td>REAL</td>
+ * </tr>
+ * <tr>
+ * <td>java.lang.Double</td>
+ * <td>DOUBLE</td>
+ * </tr>
+ * <tr>
+ * <td>java.math.BigDecimal</td>
+ * <td>DECIMAL</td>
+ * </tr>
+ * <tr>
+ * <td>java.sql.Date</td>
+ * <td>DATE</td>
+ * </tr>
+ * <tr>
+ * <td>java.sql.Time</td>
+ * <td>TIME</td>
+ * </tr>
+ * <tr>
+ * <td>java.sql.Timestamp</td>
+ * <td>TIMESTAMP</td>
+ * </tr>
+ * <tr>
+ * <td>java.util.Date</td>
+ * <td>TIMESTAMP</td>
+ * </tr>
+ * </table>
+ * <p>
+ * Unsupported data types: binary types (BLOB, etc), and custom types.
+ * <p>
+ * 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.
+ * <p>
+ * Table Interface: you may set additional parameters such as table name,
+ * primary key, and indexes in the define() method.
+ * <p>
+ * 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.
+ * <p>
+ * Automatic model generation: you may automatically generate model classes as
+ * strings with the Db and DbInspector objects:
+ *
+ * <pre>
+ * Db db = Db.open("jdbc:h2:mem:", "sa", "sa");
+ * DbInspector inspector = new DbInspector(db);
+ * List<String> models =
+ * inspector.generateModel(schema, table, packageName,
+ * annotateSchema, trimStrings)
+ * </pre>
+ *
+ * Or you may use the GenerateModels tool to generate and save your classes to
+ * the file system:
+ *
+ * <pre>
+ * java -jar iciql.jar
+ * -url "jdbc:h2:mem:"
+ * -user sa -password sa -schema schemaName -table tableName
+ * -package packageName -folder destination
+ * -annotateSchema false -trimStrings true
+ * </pre>
+ *
+ * Model validation: you may validate your model class with DbInspector object.
+ * The DbInspector will report errors, warnings, and suggestions:
+ *
+ * <pre>
+ * 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);
+ * }
+ * </pre>
+ */
+public interface Iciql {
+
+ /**
+ * An annotation for a database.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ public @interface IQDatabase {
+
+ /**
+ * 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 version() default 0;
+
+ }
+
+ /**
+ * An annotation for a schema.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ public @interface IQSchema {
+
+ /**
+ * The schema may be optionally specified. Default: unspecified.
+ */
+ String name() default "";
+
+ }
+
+ /**
+ * Enumeration defining the four index types.
+ */
+ public static enum IndexType {
+ STANDARD, UNIQUE, HASH, UNIQUE_HASH;
+ }
+
+ /**
+ * An index annotation.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ public @interface IQIndex {
+
+ /**
+ * Standard indexes may be optionally specified.
+ * <ul>
+ * <li>standard = "id, name"</li>
+ * <li>standard = "id name"</li>
+ * <li>standard = { "id name", "date" }</li>
+ * </ul>
+ * Standard indexes may still be added in the define() method if the
+ * model class is not annotated with IQTable. Default: unspecified.
+ */
+ String[] standard() default {};
+
+ /**
+ * Unique indexes may be optionally specified.
+ * <ul>
+ * <li>unique = "id, name"</li>
+ * <li>unique = "id name"</li>
+ * <li>unique = { "id name", "date" }</li>
+ * </ul>
+ * Unique indexes may still be added in the define() method if the model
+ * class is not annotated with IQTable. Default: unspecified.
+ */
+ String[] unique() default {};
+
+ /**
+ * Hash indexes may be optionally specified.
+ * <ul>
+ * <li>hash = "name"
+ * <li>hash = { "name", "date" }
+ * </ul>
+ * Hash indexes may still be added in the define() method if the model
+ * class is not annotated with IQTable. Default: unspecified.
+ */
+ String[] hash() default {};
+
+ /**
+ * Unique hash indexes may be optionally specified.
+ * <ul>
+ * <li>uniqueHash = "id"
+ * <li>uniqueHash = "name"
+ * <li>uniqueHash = { "id", "name" }
+ * </ul>
+ * Unique hash indexes may still be added in the define() method if the
+ * model class is not annotated with IQTable. Default: unspecified.
+ */
+ String[] uniqueHash() 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.
+ * <p>
+ * 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.
+ * <ul>
+ * <li>primaryKey = "id, name"
+ * <li>primaryKey = "id name"
+ * </ul>
+ * 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 createIfRequired() default true;
+
+ /**
+ * Whether only supported types are mapped. If true, unsupported mapped
+ * types will throw an IciqlException. If false, unsupported mapped
+ * types will default to VARCHAR. Default: true.
+ */
+ boolean strictTypeMapping() 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
+ * databases. Default: false.
+ */
+ boolean memoryTable() default false;
+
+ /**
+ * If non-zero, iciql will maintain 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 version() default 0;
+ }
+
+ /**
+ * 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;
+
+ /**
+ * If larger than zero, it is used during the CREATE TABLE phase. It may
+ * also be used to prevent database exceptions on INSERT and UPDATE
+ * statements (see trimString).
+ * <p>
+ * Any maxLength set in define() may override this annotation setting if
+ * the model class is not annotated with IQTable. Default: 0.
+ */
+ int maxLength() default 0;
+
+ /**
+ * If true, iciql will automatically trim the string if it exceeds
+ * maxLength (value.substring(0, maxLength)). Default: false.
+ */
+ boolean trimString() default false;
+
+ /**
+ * If false, iciql will set the column NOT NULL during the CREATE TABLE
+ * phase. Default: false.
+ */
+ boolean allowNull() default false;
+
+ /**
+ * 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:
+ * <ul>
+ * <li>defaultValue="" (null)
+ * <li>defaultValue="CURRENT_TIMESTAMP"
+ * <li>defaultValue="''" (empty string)
+ * <li>defaultValue="'0'"
+ * <li>defaultValue="'1970-01-01 00:00:01'"
+ * </ul>
+ * 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.
+ * Default: unspecified (null).
+ */
+ String defaultValue() default "";
+
+ }
+
+ /**
+ * This method is called to let the table define the primary key, indexes,
+ * and the table name.
+ */
+ @Deprecated
+ void defineIQ();
+}
diff --git a/src/com/iciql/IciqlException.java b/src/com/iciql/IciqlException.java new file mode 100644 index 0000000..2aee36d --- /dev/null +++ b/src/com/iciql/IciqlException.java @@ -0,0 +1,37 @@ +/*
+ * 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 wraps all exceptions with this class.
+ */
+public class IciqlException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ public IciqlException(String message) {
+ super(message);
+ }
+
+ public IciqlException(Throwable t) {
+ super(t);
+ }
+
+ public IciqlException(String message, Throwable t) {
+ super(message, t);
+ }
+}
diff --git a/src/com/iciql/ModelUtils.java b/src/com/iciql/ModelUtils.java new file mode 100644 index 0000000..6b28f0e --- /dev/null +++ b/src/com/iciql/ModelUtils.java @@ -0,0 +1,324 @@ +/* + * 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.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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<Class<?>, String> SUPPORTED_TYPES = new HashMap<Class<?>, String>(); + + static { + Map<Class<?>, String> m = SUPPORTED_TYPES; + m.put(String.class, "VARCHAR"); + m.put(Boolean.class, "BIT"); + 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"); + // TODO add blobs, binary types, custom types? + } + + /** + * Convert SQL type aliases to the list of supported types. This map is used + * by generation and validation. + */ + private static final Map<String, String> SQL_TYPES = new HashMap<String, String>(); + + static { + Map<String, String> 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("BOOL", "BIT"); + m.put("BOOLEAN", "BIT"); + + // 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"); + + // 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"); + + // date + m.put("DATETIME", "TIMESTAMP"); + m.put("SMALLDATETIME", "TIMESTAMP"); + } + + private static final List<String> 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 + * @param strictTypeMapping + * throws a IciqlException if type is unsupported + * @return + */ + static String getDataType(FieldDefinition fieldDef, boolean strictTypeMapping) { + Class<?> fieldClass = fieldDef.field.getType(); + if (SUPPORTED_TYPES.containsKey(fieldClass)) { + String type = SUPPORTED_TYPES.get(fieldClass); + if (type.equals("VARCHAR") && fieldDef.maxLength <= 0) { + // unspecified length strings are TEXT, not VARCHAR + return "TEXT"; + } + return type; + } + if (!strictTypeMapping) { + return "VARCHAR"; + } + 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<? extends java.util.Date> 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 (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; + } + className.append(Character.toUpperCase(chunk.charAt(0))); + className.append(chunk.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; + } + + /** + * 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; + } + + // TODO H2 single-quotes literal values, which is useful. + // MySQL does not single-quote literal values so its hard to + // differentiate a FUNCTION/VARIABLE from a literal value. + + // 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/com/iciql/OrderExpression.java b/src/com/iciql/OrderExpression.java new file mode 100644 index 0000000..36acf16 --- /dev/null +++ b/src/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 <T>
+ * the query data type
+ */
+
+class OrderExpression<T> {
+ private Query<T> query;
+ private Object expression;
+ private boolean desc;
+ private boolean nullsFirst;
+ private boolean nullsLast;
+
+ OrderExpression(Query<T> 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, expression);
+ if (desc) {
+ stat.appendSQL(" DESC");
+ }
+ if (nullsLast) {
+ stat.appendSQL(" NULLS LAST");
+ }
+ if (nullsFirst) {
+ stat.appendSQL(" NULLS FIRST");
+ }
+ }
+
+}
diff --git a/src/com/iciql/Query.java b/src/com/iciql/Query.java new file mode 100644 index 0000000..97e143b --- /dev/null +++ b/src/com/iciql/Query.java @@ -0,0 +1,451 @@ +/*
+ * 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.Clob;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+import com.iciql.bytecode.ClassReader;
+import com.iciql.util.JdbcUtils;
+import com.iciql.util.StatementLogger;
+import com.iciql.util.Utils;
+
+/**
+ * This class represents a query.
+ *
+ * @param <T>
+ * the return type
+ */
+
+public class Query<T> {
+
+ private Db db;
+ private SelectTable<T> from;
+ private ArrayList<Token> conditions = Utils.newArrayList();
+ private ArrayList<UpdateColumn> updateColumnDeclarations = Utils.newArrayList();
+ private ArrayList<SelectTable<T>> joins = Utils.newArrayList();
+ private final IdentityHashMap<Object, SelectColumn<T>> aliasMap = Utils.newIdentityHashMap();
+ private ArrayList<OrderExpression<T>> orderByList = Utils.newArrayList();
+ private Object[] groupByExpressions;
+ 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 <T> Query<T> from(Db db, T alias) {
+ Query<T> query = new Query<T>(db);
+ TableDefinition<T> def = (TableDefinition<T>) db.define(alias.getClass());
+ query.from = new SelectTable<T>(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 new IciqlException(e);
+ } finally {
+ JdbcUtils.closeSilently(rs, true);
+ }
+ }
+
+ public List<T> select() {
+ return select(false);
+ }
+
+ public T selectFirst() {
+ return select(false).get(0);
+ }
+
+ public List<T> selectDistinct() {
+ return select(true);
+ }
+
+ @SuppressWarnings("unchecked")
+ public <X, Z> X selectFirst(Z x) {
+ List<X> list = (List<X>) select(x);
+ return list.isEmpty() ? null : list.get(0);
+ }
+
+ public String getSQL() {
+ SQLStatement stat = getSelectStatement(false);
+ stat.appendSQL("*");
+ appendFromWhere(stat);
+ return stat.getSQL().trim();
+ }
+
+ private List<T> select(boolean distinct) {
+ List<T> result = Utils.newArrayList();
+ TableDefinition<T> def = from.getAliasDefinition();
+ SQLStatement stat = getSelectStatement(distinct);
+ def.appendSelectList(stat);
+ appendFromWhere(stat);
+ ResultSet rs = stat.executeQuery();
+ try {
+ while (rs.next()) {
+ T item = from.newObject();
+ from.getAliasDefinition().readRow(item, rs);
+ result.add(item);
+ }
+ } catch (SQLException e) {
+ throw new IciqlException(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);
+ StatementLogger.delete(stat.getSQL());
+ return stat.executeUpdate();
+ }
+
+ public <A> UpdateColumnSet<T, A> set(A field) {
+ return new UpdateColumnSet<T, A>(this, field);
+ }
+
+ public <A> UpdateColumnIncrement<T, A> increment(A field) {
+ return new UpdateColumnIncrement<T, A>(this, field);
+ }
+
+ 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);
+ StatementLogger.update(stat.getSQL());
+ return stat.executeUpdate();
+ }
+
+ public <X, Z> List<X> selectDistinct(Z x) {
+ return select(x, true);
+ }
+
+ public <X, Z> List<X> select(Z x) {
+ return select(x, false);
+ }
+
+ @SuppressWarnings("unchecked")
+ private <X, Z> List<X> select(Z x, boolean distinct) {
+ Class<?> clazz = x.getClass();
+ if (Utils.isSimpleType(clazz)) {
+ return selectSimple((X) x, distinct);
+ }
+ clazz = clazz.getSuperclass();
+ return select((Class<X>) clazz, (X) x, distinct);
+ }
+
+ private <X> List<X> select(Class<X> clazz, X x, boolean distinct) {
+ List<X> result = Utils.newArrayList();
+ TableDefinition<X> def = db.define(clazz);
+ SQLStatement stat = getSelectStatement(distinct);
+ def.appendSelectList(stat, this, x);
+ appendFromWhere(stat);
+ ResultSet rs = stat.executeQuery();
+ try {
+ while (rs.next()) {
+ X row = Utils.newObject(clazz);
+ def.readRow(row, rs);
+ result.add(row);
+ }
+ } catch (SQLException e) {
+ throw new IciqlException(e);
+ } finally {
+ JdbcUtils.closeSilently(rs, true);
+ }
+ return result;
+ }
+
+ @SuppressWarnings("unchecked")
+ private <X> List<X> selectSimple(X x, boolean distinct) {
+ SQLStatement stat = getSelectStatement(distinct);
+ appendSQL(stat, x);
+ appendFromWhere(stat);
+ ResultSet rs = stat.executeQuery();
+ List<X> result = Utils.newArrayList();
+ try {
+ while (rs.next()) {
+ try {
+ X value;
+ Object o = rs.getObject(1);
+ if (Clob.class.isAssignableFrom(o.getClass())) {
+ value = (X) Utils.convert(o, String.class);
+ } else {
+ value = (X) o;
+ }
+ result.add(value);
+ } catch (Exception e) {
+ throw new IciqlException(e);
+ }
+ }
+ } catch (SQLException e) {
+ throw new IciqlException(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;
+ }
+
+ public <A> QueryCondition<T, A> where(A x) {
+ return new QueryCondition<T, A>(this, x);
+ }
+
+ public <A> QueryWhere<T> where(Filter filter) {
+ HashMap<String, Object> fieldMap = Utils.newHashMap();
+ for (Field f : filter.getClass().getDeclaredFields()) {
+ f.setAccessible(true);
+ try {
+ Object obj = f.get(filter);
+ if (obj == from.getAlias()) {
+ List<TableDefinition.FieldDefinition> 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<T>(this);
+ }
+
+ public QueryWhere<T> where(String fragment, Object... args) {
+ conditions.add(new RuntimeToken(fragment, args));
+ return new QueryWhere<T>(this);
+ }
+
+ public QueryWhere<T> whereTrue(Boolean condition) {
+ Token token = new Function("", condition);
+ addConditionToken(token);
+ return new QueryWhere<T>(this);
+ }
+
+ /**
+ * Sets the Limit and Offset of a query.
+ *
+ * @return the query
+ */
+
+ public Query<T> limit(long limit) {
+ this.limit = limit;
+ return this;
+ }
+
+ public Query<T> offset(long offset) {
+ this.offset = offset;
+ return this;
+ }
+
+ /**
+ * Order by a number of columns.
+ *
+ * @param expressions
+ * the columns
+ * @return the query
+ */
+
+ public Query<T> orderBy(Object... expressions) {
+ for (Object expr : expressions) {
+ OrderExpression<T> e = new OrderExpression<T>(this, expr, false, false, false);
+ addOrderBy(e);
+ }
+ return this;
+ }
+
+ public Query<T> orderByDesc(Object expr) {
+ OrderExpression<T> e = new OrderExpression<T>(this, expr, true, false, false);
+ addOrderBy(e);
+ return this;
+ }
+
+ public Query<T> groupBy(Object... groupBy) {
+ this.groupByExpressions = groupBy;
+ return this;
+ }
+
+ /**
+ * INTERNAL
+ *
+ * @param stat
+ * the statement
+ * @param x
+ * the alias object
+ */
+ public void appendSQL(SQLStatement stat, Object x) {
+ if (x == Function.count()) {
+ stat.appendSQL("COUNT(*)");
+ return;
+ }
+ Token token = Db.getToken(x);
+ if (token != null) {
+ token.appendSQL(stat, this);
+ return;
+ }
+ SelectColumn<T> col = aliasMap.get(x);
+ if (col != null) {
+ col.appendSQL(stat);
+ return;
+ }
+ stat.appendSQL("?");
+ stat.addParameter(x);
+ }
+
+ 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) {
+ stat.appendSQL(" FROM ");
+ from.appendSQL(stat);
+ for (SelectTable<T> join : joins) {
+ join.appendSQLAsJoin(stat, this);
+ }
+ appendWhere(stat);
+ if (groupByExpressions != null) {
+ stat.appendSQL(" GROUP BY ");
+ int i = 0;
+ for (Object obj : groupByExpressions) {
+ if (i++ > 0) {
+ stat.appendSQL(", ");
+ }
+ appendSQL(stat, obj);
+ stat.appendSQL(" ");
+ }
+ }
+ if (!orderByList.isEmpty()) {
+ stat.appendSQL(" ORDER BY ");
+ int i = 0;
+ for (OrderExpression<T> o : orderByList) {
+ if (i++ > 0) {
+ stat.appendSQL(", ");
+ }
+ o.appendSQL(stat);
+ stat.appendSQL(" ");
+ }
+ }
+ if (limit > 0) {
+ db.getDialect().appendLimit(stat, limit);
+ }
+ if (offset > 0) {
+ db.getDialect().appendOffset(stat, offset);
+ }
+ StatementLogger.select(stat.getSQL());
+ }
+
+ /**
+ * Join another table.
+ *
+ * @param alias
+ * an alias for the table to join
+ * @return the joined query
+ */
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public <U> QueryJoin innerJoin(U alias) {
+ TableDefinition<T> def = (TableDefinition<T>) db.define(alias.getClass());
+ SelectTable<T> join = new SelectTable(db, this, alias, false);
+ def.initSelectObject(join, alias, aliasMap);
+ joins.add(join);
+ return new QueryJoin(this, join);
+ }
+
+ Db getDb() {
+ return db;
+ }
+
+ boolean isJoin() {
+ return !joins.isEmpty();
+ }
+
+ SelectColumn<T> getSelectColumn(Object obj) {
+ return aliasMap.get(obj);
+ }
+
+ void addOrderBy(OrderExpression<T> expr) {
+ orderByList.add(expr);
+ }
+
+}
diff --git a/src/com/iciql/QueryCondition.java b/src/com/iciql/QueryCondition.java new file mode 100644 index 0000000..8e7cd42 --- /dev/null +++ b/src/com/iciql/QueryCondition.java @@ -0,0 +1,69 @@ +/* + * 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 <T> + * the return type of the query + * @param <A> + * the incomplete condition data type + */ + +public class QueryCondition<T, A> { + + private Query<T> query; + private A x; + + QueryCondition(Query<T> query, A x) { + this.query = query; + this.x = x; + } + + public QueryWhere<T> is(A y) { + query.addConditionToken(new Condition<A>(x, y, CompareType.EQUAL)); + return new QueryWhere<T>(query); + } + + public QueryWhere<T> exceeds(A y) { + query.addConditionToken(new Condition<A>(x, y, CompareType.EXCEEDS)); + return new QueryWhere<T>(query); + } + + public QueryWhere<T> atLeast(A y) { + query.addConditionToken(new Condition<A>(x, y, CompareType.AT_LEAST)); + return new QueryWhere<T>(query); + } + + public QueryWhere<T> lessThan(A y) { + query.addConditionToken(new Condition<A>(x, y, CompareType.LESS_THAN)); + return new QueryWhere<T>(query); + } + + public QueryWhere<T> atMost(A y) { + query.addConditionToken(new Condition<A>(x, y, CompareType.AT_MOST)); + return new QueryWhere<T>(query); + } + + public QueryWhere<T> like(A pattern) { + query.addConditionToken(new Condition<A>(x, pattern, CompareType.LIKE)); + return new QueryWhere<T>(query); + } + +} diff --git a/src/com/iciql/QueryJoin.java b/src/com/iciql/QueryJoin.java new file mode 100644 index 0000000..bb614eb --- /dev/null +++ b/src/com/iciql/QueryJoin.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;
+
+/**
+ * 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 <A> QueryJoinCondition<A> on(A x) {
+ return new QueryJoinCondition<A>(query, join, x);
+ }
+}
diff --git a/src/com/iciql/QueryJoinCondition.java b/src/com/iciql/QueryJoinCondition.java new file mode 100644 index 0000000..e5620d5 --- /dev/null +++ b/src/com/iciql/QueryJoinCondition.java @@ -0,0 +1,43 @@ +/*
+ * 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 <A>
+ * the incomplete condition data type
+ */
+
+public class QueryJoinCondition<A> {
+
+ 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(A y) {
+ join.addConditionToken(new Condition<A>(x, y, CompareType.EQUAL));
+ return query;
+ }
+}
diff --git a/src/com/iciql/QueryWhere.java b/src/com/iciql/QueryWhere.java new file mode 100644 index 0000000..9071b52 --- /dev/null +++ b/src/com/iciql/QueryWhere.java @@ -0,0 +1,148 @@ +/*
+ * 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 <T>
+ * the return type
+ */
+
+public class QueryWhere<T> {
+
+ Query<T> query;
+
+ QueryWhere(Query<T> query) {
+ this.query = query;
+ }
+
+ public <A> QueryCondition<T, A> and(A x) {
+ query.addConditionToken(ConditionAndOr.AND);
+ return new QueryCondition<T, A>(query, x);
+ }
+
+ public <A> QueryCondition<T, A> or(A x) {
+ query.addConditionToken(ConditionAndOr.OR);
+ return new QueryCondition<T, A>(query, x);
+ }
+
+ public QueryWhere<T> limit(long limit) {
+ query.limit(limit);
+ return this;
+ }
+
+ public QueryWhere<T> offset(long offset) {
+ query.offset(offset);
+ return this;
+ }
+
+ public <X, Z> List<X> select(Z x) {
+ return query.select(x);
+ }
+
+ public String getSQL() {
+ SQLStatement stat = new SQLStatement(query.getDb());
+ stat.appendSQL("SELECT *");
+ query.appendFromWhere(stat);
+ return stat.getSQL().trim();
+ }
+
+ public <X, Z> List<X> selectDistinct(Z x) {
+ return query.selectDistinct(x);
+ }
+
+ public <X, Z> X selectFirst(Z x) {
+ List<X> list = query.select(x);
+ return list.isEmpty() ? null : list.get(0);
+ }
+
+ public List<T> select() {
+ return query.select();
+ }
+
+ public T selectFirst() {
+ List<T> list = select();
+ return list.isEmpty() ? null : list.get(0);
+ }
+
+ public List<T> selectDistinct() {
+ return query.selectDistinct();
+ }
+
+ /**
+ * Order by a number of columns.
+ *
+ * @param expressions
+ * the order by expressions
+ * @return the query
+ */
+
+ public QueryWhere<T> orderBy(Object... expressions) {
+ for (Object expr : expressions) {
+ OrderExpression<T> e = new OrderExpression<T>(query, expr, false, false, false);
+ query.addOrderBy(e);
+ }
+ return this;
+ }
+
+ public QueryWhere<T> orderByNullsFirst(Object expr) {
+ OrderExpression<T> e = new OrderExpression<T>(query, expr, false, true, false);
+ query.addOrderBy(e);
+ return this;
+ }
+
+ public QueryWhere<T> orderByNullsLast(Object expr) {
+ OrderExpression<T> e = new OrderExpression<T>(query, expr, false, false, true);
+ query.addOrderBy(e);
+ return this;
+ }
+
+ public QueryWhere<T> orderByDesc(Object expr) {
+ OrderExpression<T> e = new OrderExpression<T>(query, expr, true, false, false);
+ query.addOrderBy(e);
+ return this;
+ }
+
+ public QueryWhere<T> orderByDescNullsFirst(Object expr) {
+ OrderExpression<T> e = new OrderExpression<T>(query, expr, true, true, false);
+ query.addOrderBy(e);
+ return this;
+ }
+
+ public QueryWhere<T> orderByDescNullsLast(Object expr) {
+ OrderExpression<T> e = new OrderExpression<T>(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/com/iciql/RuntimeToken.java b/src/com/iciql/RuntimeToken.java new file mode 100644 index 0000000..cbfd882 --- /dev/null +++ b/src/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 <T> void appendSQL(SQLStatement stat, Query<T> 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/com/iciql/SQLDialect.java b/src/com/iciql/SQLDialect.java new file mode 100644 index 0000000..3cb9339 --- /dev/null +++ b/src/com/iciql/SQLDialect.java @@ -0,0 +1,236 @@ +/* + * 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.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +import com.iciql.TableDefinition.IndexDefinition; +import com.iciql.util.StatementBuilder; +import com.iciql.util.StringUtils; + +/** + * This interface defines points where iciql can build different statements + * depending on the database used. + */ +public interface SQLDialect { + + /** + * Configure the dialect from the database connection. + * + * @param conn + */ + void configureDialect(Connection conn); + + /** + * Returns a properly formatted table name for the dialect. + * + * @param schema + * the schema name, or null for no schema + * @param table + * the properly formatted table name + * @return the SQL snippet + */ + String prepareTableName(String schema, String table); + + /** + * 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 INDEX statement. + * + * @param schema + * the schema name + * @param table + * the table name + * @param index + * the index definition + * @return the SQL statement + */ + String prepareCreateIndex(String schema, String table, IndexDefinition index); + + /** + * Append "LIMIT limit" to the SQL statement. + * + * @param stat + * the statement + * @param limit + * the limit + */ + void appendLimit(SQLStatement stat, long limit); + + /** + * Append "OFFSET offset" to the SQL statement. + * + * @param stat + * the statement + * @param offset + * the offset + */ + void appendOffset(SQLStatement stat, long offset); + + /** + * Whether memory tables are supported. + * + * @return true if they are + */ + boolean supportsMemoryTables(); + + /** + * Whether merge is a supported function. + * + * @return true if they are + */ + boolean supportsMerge(); + + /** + * Whether LIMIT/OFFSET notation is supported. + * + * @return true if they are + */ + boolean supportsLimitOffset(); + + /** + * Default implementation of an SQL dialect. + * Does not support merge nor index creation. + */ + public static class DefaultSQLDialect implements SQLDialect { + float databaseVersion; + String productName; + String productVersion; + + @Override + public String toString() { + return getClass().getName() + ": " + productName + " " + productVersion; + } + + @Override + public void configureDialect(Connection conn) { + loadIdentity(conn); + } + + protected void loadIdentity(Connection conn) { + try { + DatabaseMetaData data = conn.getMetaData(); + databaseVersion = Float.parseFloat(data.getDatabaseMajorVersion() + "." + + data.getDatabaseMinorVersion()); + productName = data.getDatabaseProductName(); + productVersion = data.getDatabaseProductVersion(); + } catch (SQLException e) { + throw new IciqlException(e); + } + } + + @Override + public boolean supportsMemoryTables() { + return false; + } + + @Override + public boolean supportsMerge() { + return false; + } + + @Override + public boolean supportsLimitOffset() { + return true; + } + + @Override + public String prepareTableName(String schema, String table) { + if (StringUtils.isNullOrEmpty(schema)) { + return table; + } + return schema + "." + table; + } + + @Override + public String prepareColumnName(String name) { + return name; + } + + @Override + public String prepareCreateIndex(String schema, String table, IndexDefinition index) { + throw new IciqlException("Dialect does not support index creation!"); + } + + @Override + public void appendLimit(SQLStatement stat, long limit) { + stat.appendSQL(" LIMIT " + limit); + } + + @Override + public void appendOffset(SQLStatement stat, long offset) { + stat.appendSQL(" OFFSET " + offset); + } + } + + + /** + * H2 database dialect. + */ + public static class H2Dialect extends DefaultSQLDialect { + + @Override + public boolean supportsMemoryTables() { + return true; + } + + @Override + public boolean supportsMerge() { + return true; + } + + @Override + public String prepareCreateIndex(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(")"); + return buff.toString(); + } + } +} diff --git a/src/com/iciql/SQLStatement.java b/src/com/iciql/SQLStatement.java new file mode 100644 index 0000000..b6c5e18 --- /dev/null +++ b/src/com/iciql/SQLStatement.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;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+
+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<Object> params = new ArrayList<Object>();
+
+ SQLStatement(Db db) {
+ this.db = db;
+ }
+
+ 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));
+ }
+
+ String getSQL() {
+ if (sql == null) {
+ sql = buff.toString();
+ }
+ return sql;
+ }
+
+ SQLStatement addParameter(Object o) {
+ params.add(o);
+ return this;
+ }
+
+ ResultSet executeQuery() {
+ try {
+ return prepare(false).executeQuery();
+ } catch (SQLException e) {
+ throw new IciqlException(e);
+ }
+ }
+
+ int executeUpdate() {
+ PreparedStatement ps = null;
+ try {
+ ps = prepare(false);
+ return ps.executeUpdate();
+ } catch (SQLException e) {
+ throw new IciqlException(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 new IciqlException(e);
+ } finally {
+ JdbcUtils.closeSilently(ps);
+ }
+ }
+
+ private static void setValue(PreparedStatement prep, int parameterIndex, Object x) {
+ try {
+ prep.setObject(parameterIndex, x);
+ } catch (SQLException e) {
+ throw new IciqlException(e);
+ }
+ }
+
+ private 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/com/iciql/SelectColumn.java b/src/com/iciql/SelectColumn.java new file mode 100644 index 0000000..43a1a93 --- /dev/null +++ b/src/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 <T>
+ * the table data type
+ */
+
+class SelectColumn<T> {
+ private SelectTable<T> selectTable;
+ private FieldDefinition fieldDef;
+
+ SelectColumn(SelectTable<T> 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<T> getSelectTable() {
+ return selectTable;
+ }
+
+ Object getCurrentValue() {
+ return fieldDef.getValue(selectTable.getCurrent());
+ }
+}
diff --git a/src/com/iciql/SelectTable.java b/src/com/iciql/SelectTable.java new file mode 100644 index 0000000..7c5017c --- /dev/null +++ b/src/com/iciql/SelectTable.java @@ -0,0 +1,113 @@ +/*
+ * 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 <T>
+ * the table class
+ */
+
+class SelectTable<T> {
+
+ private static int asCounter;
+ private Query<T> query;
+ private Class<T> clazz;
+ private T current;
+ private String as;
+ private TableDefinition<T> aliasDef;
+ private boolean outerJoin;
+ private ArrayList<Token> joinConditions = Utils.newArrayList();
+ private T alias;
+
+ @SuppressWarnings("unchecked")
+ SelectTable(Db db, Query<T> query, T alias, boolean outerJoin) {
+ this.alias = alias;
+ this.query = query;
+ this.outerJoin = outerJoin;
+ aliasDef = (TableDefinition<T>) db.getTableDefinition(alias.getClass());
+ clazz = Utils.getClass(alias);
+ as = "T" + asCounter++;
+ }
+
+ T getAlias() {
+ return alias;
+ }
+
+ T newObject() {
+ return Utils.newObject(clazz);
+ }
+
+ TableDefinition<T> 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<T> 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<T> 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/com/iciql/TableDefinition.java b/src/com/iciql/TableDefinition.java new file mode 100644 index 0000000..883ce33 --- /dev/null +++ b/src/com/iciql/TableDefinition.java @@ -0,0 +1,669 @@ +/*
+ * 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.lang.reflect.Modifier;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.iciql.Iciql.IndexType;
+import com.iciql.Iciql.IQColumn;
+import com.iciql.Iciql.IQIndex;
+import com.iciql.Iciql.IQSchema;
+import com.iciql.Iciql.IQTable;
+import com.iciql.util.StatementBuilder;
+import com.iciql.util.StatementLogger;
+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 <T>
+ * the table type
+ */
+
+class TableDefinition<T> {
+
+ /**
+ * The meta data of an index.
+ */
+
+ static class IndexDefinition {
+ IndexType type;
+ String indexName;
+
+ List<String> columnNames;
+ }
+
+ /**
+ * The meta data of a field.
+ */
+
+ static class FieldDefinition {
+ String columnName;
+ Field field;
+ String dataType;
+ int maxLength;
+ boolean isPrimaryKey;
+ boolean isAutoIncrement;
+ boolean trimString;
+ boolean allowNull;
+ String defaultValue;
+
+ Object getValue(Object obj) {
+ try {
+ return field.get(obj);
+ } catch (Exception e) {
+ throw new IciqlException(e);
+ }
+ }
+
+ Object initWithNewObject(Object obj) {
+ Object o = Utils.newObject(field.getType());
+ setValue(obj, o);
+ return o;
+ }
+
+ void setValue(Object obj, Object o) {
+ try {
+ if (!field.isAccessible()) {
+ field.setAccessible(true);
+ }
+ o = Utils.convert(o, field.getType());
+ field.set(obj, o);
+ } catch (Exception e) {
+ throw new IciqlException(e);
+ }
+ }
+
+ Object read(ResultSet rs, int columnIndex) {
+ try {
+ return rs.getObject(columnIndex);
+ } catch (SQLException e) {
+ throw new IciqlException(e);
+ }
+ }
+ }
+
+ String schemaName;
+ String tableName;
+ int tableVersion;
+ private boolean createTableIfRequired = true;
+ private Class<T> clazz;
+ private ArrayList<FieldDefinition> fields = Utils.newArrayList();
+ private IdentityHashMap<Object, FieldDefinition> fieldMap = Utils.newIdentityHashMap();
+
+ private List<String> primaryKeyColumnNames;
+ private ArrayList<IndexDefinition> indexes = Utils.newArrayList();
+ private boolean memoryTable;
+
+ TableDefinition(Class<T> clazz) {
+ this.clazz = clazz;
+ schemaName = null;
+ tableName = clazz.getSimpleName();
+ }
+
+ Class<T> getModelClass() {
+ return clazz;
+ }
+
+ List<FieldDefinition> getFields() {
+ return fields;
+ }
+
+ FieldDefinition getField(String name) {
+ for (FieldDefinition field:fields) {
+ if (field.columnName.equalsIgnoreCase(name)) {
+ return field;
+ }
+ }
+ return null;
+ }
+
+ void setSchemaName(String schemaName) {
+ this.schemaName = schemaName;
+ }
+
+ void setTableName(String tableName) {
+ this.tableName = tableName;
+ }
+
+ /**
+ * Define a primary key by the specified model fields.
+ *
+ * @param modelFields
+ * the ordered list of model fields
+ */
+ void setPrimaryKey(Object[] modelFields) {
+ List<String> columnNames = mapColumnNames(modelFields);
+ setPrimaryKey(columnNames);
+ }
+
+ /**
+ * Define a primary key by the specified column names.
+ *
+ * @param columnNames
+ * the ordered list of column names
+ */
+ void setPrimaryKey(List<String> columnNames) {
+ primaryKeyColumnNames = Utils.newArrayList(columnNames);
+ // set isPrimaryKey flag for all field definitions
+ for (FieldDefinition fieldDefinition : fieldMap.values()) {
+ fieldDefinition.isPrimaryKey = this.primaryKeyColumnNames.contains(fieldDefinition.columnName);
+ }
+ }
+
+ <A> String getColumnName(A fieldObject) {
+ FieldDefinition def = fieldMap.get(fieldObject);
+ return def == null ? null : def.columnName;
+ }
+
+ private ArrayList<String> mapColumnNames(Object[] columns) {
+ ArrayList<String> columnNames = Utils.newArrayList();
+ for (Object column : columns) {
+ columnNames.add(getColumnName(column));
+ }
+ return columnNames;
+ }
+
+ /**
+ * Defines an index with the specified model fields.
+ *
+ * @param type
+ * the index type (STANDARD, HASH, UNIQUE, UNIQUE_HASH)
+ * @param modelFields
+ * the ordered list of model fields
+ */
+ void addIndex(IndexType type, Object[] modelFields) {
+ List<String> columnNames = mapColumnNames(modelFields);
+ addIndex(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
+ */
+ void addIndex(IndexType type, List<String> columnNames) {
+ IndexDefinition index = new IndexDefinition();
+ index.indexName = tableName + "_" + indexes.size();
+ index.columnNames = Utils.newArrayList(columnNames);
+ index.type = type;
+ indexes.add(index);
+ }
+
+ public void setColumnName(Object column, String columnName) {
+ FieldDefinition def = fieldMap.get(column);
+ if (def != null) {
+ def.columnName = columnName;
+ }
+ }
+
+ public void setMaxLength(Object column, int maxLength) {
+ FieldDefinition def = fieldMap.get(column);
+ if (def != null) {
+ def.maxLength = maxLength;
+ }
+ }
+
+ void mapFields() {
+ boolean byAnnotationsOnly = false;
+ boolean inheritColumns = false;
+ boolean strictTypeMapping = false;
+ if (clazz.isAnnotationPresent(IQTable.class)) {
+ IQTable tableAnnotation = clazz.getAnnotation(IQTable.class);
+ byAnnotationsOnly = tableAnnotation.annotationsOnly();
+ inheritColumns = tableAnnotation.inheritColumns();
+ strictTypeMapping = tableAnnotation.strictTypeMapping();
+ }
+
+ List<Field> classFields = Utils.newArrayList();
+ classFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
+ if (inheritColumns) {
+ Class<?> superClass = clazz.getSuperclass();
+ classFields.addAll(Arrays.asList(superClass.getDeclaredFields()));
+ }
+
+ for (Field f : classFields) {
+ // default to field name
+ String columnName = f.getName();
+ boolean isAutoIncrement = false;
+ boolean isPrimaryKey = false;
+ int maxLength = 0;
+ boolean trimString = false;
+ boolean allowNull = true;
+ String defaultValue = "";
+ 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();
+ maxLength = col.maxLength();
+ trimString = col.trimString();
+ allowNull = col.allowNull();
+ defaultValue = col.defaultValue();
+ }
+ boolean isPublic = Modifier.isPublic(f.getModifiers());
+ boolean reflectiveMatch = isPublic && !byAnnotationsOnly;
+ if (reflectiveMatch || hasAnnotation) {
+ FieldDefinition fieldDef = new FieldDefinition();
+ fieldDef.field = f;
+ fieldDef.columnName = columnName;
+ fieldDef.isAutoIncrement = isAutoIncrement;
+ fieldDef.isPrimaryKey = isPrimaryKey;
+ fieldDef.maxLength = maxLength;
+ fieldDef.trimString = trimString;
+ fieldDef.allowNull = allowNull;
+ fieldDef.defaultValue = defaultValue;
+ fieldDef.dataType = ModelUtils.getDataType(fieldDef, strictTypeMapping);
+ fields.add(fieldDef);
+ }
+ }
+ List<String> primaryKey = Utils.newArrayList();
+ for (FieldDefinition fieldDef : fields) {
+ if (fieldDef.isPrimaryKey) {
+ primaryKey.add(fieldDef.columnName);
+ }
+ }
+ if (primaryKey.size() > 0) {
+ setPrimaryKey(primaryKey);
+ }
+ }
+
+ /**
+ * Optionally truncates strings to the maximum length
+ */
+ private Object getValue(Object obj, FieldDefinition field) {
+ Object value = field.getValue(obj);
+ if (field.trimString && field.maxLength > 0) {
+ if (value instanceof String) {
+ // clip strings
+ String s = (String) value;
+ if (s.length() > field.maxLength) {
+ return s.substring(0, field.maxLength);
+ }
+ return s;
+ }
+ return value;
+ }
+ // standard behavior
+ return value;
+ }
+
+ long insert(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) {
+ buff.appendExceptFirst(", ");
+ buff.append(db.getDialect().prepareColumnName(field.columnName));
+ }
+ buff.append(") VALUES(");
+ buff.resetCount();
+ for (FieldDefinition field : fields) {
+ buff.appendExceptFirst(", ");
+ buff.append('?');
+ Object value = getValue(obj, field);
+ stat.addParameter(value);
+ }
+ buff.append(')');
+ stat.setSQL(buff.toString());
+ StatementLogger.insert(stat.getSQL());
+ if (returnKey) {
+ return stat.executeInsert();
+ }
+ return stat.executeUpdate();
+ }
+
+ void 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);
+ StatementBuilder buff = new StatementBuilder("MERGE INTO ");
+ buff.append(db.getDialect().prepareTableName(schemaName, tableName)).append(" (");
+ buff.resetCount();
+ for (FieldDefinition field : fields) {
+ buff.appendExceptFirst(", ");
+ buff.append(db.getDialect().prepareColumnName(field.columnName));
+ }
+ buff.append(") KEY(");
+ buff.resetCount();
+ for (FieldDefinition field : fields) {
+ if (field.isPrimaryKey) {
+ buff.appendExceptFirst(", ");
+ buff.append(db.getDialect().prepareColumnName(field.columnName));
+ }
+ }
+ buff.append(") ");
+ buff.resetCount();
+ buff.append("VALUES (");
+ for (FieldDefinition field : fields) {
+ buff.appendExceptFirst(", ");
+ buff.append('?');
+ Object value = getValue(obj, field);
+ stat.addParameter(value);
+ }
+ buff.append(')');
+ stat.setSQL(buff.toString());
+
+ StatementLogger.merge(stat.getSQL());
+ stat.executeUpdate();
+ }
+
+ void update(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);
+ StatementBuilder buff = new StatementBuilder("UPDATE ");
+ buff.append(db.getDialect().prepareTableName(schemaName, tableName)).append(" SET ");
+ buff.resetCount();
+
+ for (FieldDefinition field : fields) {
+ if (!field.isPrimaryKey) {
+ buff.appendExceptFirst(", ");
+ buff.append(db.getDialect().prepareColumnName(field.columnName));
+ buff.append(" = ?");
+ Object value = getValue(obj, field);
+ stat.addParameter(value);
+ }
+ }
+ Object alias = Utils.newObject(obj.getClass());
+ Query<Object> query = Query.from(db, alias);
+ boolean firstCondition = true;
+ for (FieldDefinition field : fields) {
+ if (field.isPrimaryKey) {
+ Object aliasValue = field.getValue(alias);
+ Object value = field.getValue(obj);
+ if (!firstCondition) {
+ query.addConditionToken(ConditionAndOr.AND);
+ }
+ firstCondition = false;
+ query.addConditionToken(new Condition<Object>(aliasValue, value, CompareType.EQUAL));
+ }
+ }
+ stat.setSQL(buff.toString());
+ query.appendWhere(stat);
+ StatementLogger.update(stat.getSQL());
+ stat.executeUpdate();
+ }
+
+ void delete(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);
+ StatementBuilder buff = new StatementBuilder("DELETE FROM ");
+ buff.append(db.getDialect().prepareTableName(schemaName, tableName));
+ buff.resetCount();
+ Object alias = Utils.newObject(obj.getClass());
+ Query<Object> query = Query.from(db, alias);
+ boolean firstCondition = true;
+ for (FieldDefinition field : fields) {
+ if (field.isPrimaryKey) {
+ Object aliasValue = field.getValue(alias);
+ Object value = field.getValue(obj);
+ if (!firstCondition) {
+ query.addConditionToken(ConditionAndOr.AND);
+ }
+ firstCondition = false;
+ query.addConditionToken(new Condition<Object>(aliasValue, value, CompareType.EQUAL));
+ }
+ }
+ stat.setSQL(buff.toString());
+ query.appendWhere(stat);
+ StatementLogger.delete(stat.getSQL());
+ stat.executeUpdate();
+ }
+
+ TableDefinition<T> createTableIfRequired(Db db) {
+ if (!createTableIfRequired) {
+ // skip table and index creation
+ // but still check for upgrades
+ db.upgradeTable(this);
+ return this;
+ }
+ SQLDialect dialect = db.getDialect();
+ SQLStatement stat = new SQLStatement(db);
+ StatementBuilder buff;
+ if (memoryTable && dialect.supportsMemoryTables()) {
+ buff = new StatementBuilder("CREATE MEMORY TABLE IF NOT EXISTS ");
+ } else {
+ buff = new StatementBuilder("CREATE TABLE IF NOT EXISTS ");
+ }
+
+ buff.append(dialect.prepareTableName(schemaName, tableName)).append('(');
+
+ for (FieldDefinition field : fields) {
+ buff.appendExceptFirst(", ");
+ buff.append(dialect.prepareColumnName(field.columnName)).append(' ').append(field.dataType);
+ if (field.maxLength > 0) {
+ buff.append('(').append(field.maxLength).append(')');
+ }
+
+ if (field.isAutoIncrement) {
+ buff.append(" AUTO_INCREMENT");
+ }
+
+ if (!field.allowNull) {
+ buff.append(" NOT NULL");
+ }
+
+ // 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);
+ }
+ }
+ }
+ }
+
+ // primary key
+ if (primaryKeyColumnNames != null && primaryKeyColumnNames.size() > 0) {
+ buff.append(", PRIMARY KEY(");
+ buff.resetCount();
+ for (String n : primaryKeyColumnNames) {
+ buff.appendExceptFirst(", ");
+ buff.append(n);
+ }
+ buff.append(')');
+ }
+ buff.append(')');
+ stat.setSQL(buff.toString());
+ StatementLogger.create(stat.getSQL());
+ stat.executeUpdate();
+
+ // create indexes
+ for (IndexDefinition index : indexes) {
+ String sql = db.getDialect().prepareCreateIndex(schemaName, tableName, index);
+ stat.setSQL(sql);
+ StatementLogger.create(stat.getSQL());
+ stat.executeUpdate();
+ }
+
+ // tables are created using IF NOT EXISTS
+ // but we may still need to upgrade
+ db.upgradeTable(this);
+ return this;
+ }
+
+ /**
+ * Retrieve list of columns from index definition.
+ *
+ * @param index
+ * the index columns, separated by space
+ * @return the column list
+ */
+ private List<String> getColumns(String index) {
+ List<String> cols = Utils.newArrayList();
+ if (index == null || index.length() == 0) {
+ return null;
+ }
+ String[] cs = index.split("(,|\\s)");
+ for (String c : cs) {
+ if (c != null && c.trim().length() > 0) {
+ cols.add(c.trim());
+ }
+ }
+ if (cols.size() == 0) {
+ return null;
+ }
+ return cols;
+ }
+
+ 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.name())) {
+ schemaName = schemaAnnotation.name();
+ }
+ }
+
+ 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()
+ createTableIfRequired = tableAnnotation.createIfRequired();
+
+ // model version
+ if (tableAnnotation.version() > 0) {
+ tableVersion = tableAnnotation.version();
+ }
+
+ // setup the primary index, if properly annotated
+ List<String> primaryKey = getColumns(tableAnnotation.primaryKey());
+ if (primaryKey != null) {
+ setPrimaryKey(primaryKey);
+ }
+ }
+
+ if (clazz.isAnnotationPresent(IQIndex.class)) {
+ IQIndex indexAnnotation = clazz.getAnnotation(IQIndex.class);
+
+ // setup the indexes, if properly annotated
+ addIndexes(IndexType.STANDARD, indexAnnotation.standard());
+ addIndexes(IndexType.UNIQUE, indexAnnotation.unique());
+ addIndexes(IndexType.HASH, indexAnnotation.hash());
+ addIndexes(IndexType.UNIQUE_HASH, indexAnnotation.uniqueHash());
+ }
+ }
+
+ void addIndexes(IndexType type, String[] indexes) {
+ for (String index : indexes) {
+ List<String> validatedColumns = getColumns(index);
+ if (validatedColumns == null) {
+ return;
+ }
+ addIndex(type, validatedColumns);
+ }
+ }
+
+ List<IndexDefinition> getIndexes(IndexType type) {
+ List<IndexDefinition> list = Utils.newArrayList();
+ for (IndexDefinition def : indexes) {
+ if (def.type.equals(type)) {
+ list.add(def);
+ }
+ }
+ return list;
+ }
+
+ void initObject(Object obj, Map<Object, FieldDefinition> map) {
+ for (FieldDefinition def : fields) {
+ Object newValue = def.initWithNewObject(obj);
+ map.put(newValue, def);
+ }
+ }
+
+ void initSelectObject(SelectTable<T> table, Object obj, Map<Object, SelectColumn<T>> map) {
+ for (FieldDefinition def : fields) {
+ Object newValue = def.initWithNewObject(obj);
+ SelectColumn<T> column = new SelectColumn<T>(table, def);
+ map.put(newValue, column);
+ }
+ }
+
+ void readRow(Object item, ResultSet rs) {
+ for (int i = 0; i < fields.size(); i++) {
+ FieldDefinition def = fields.get(i);
+ Object o = def.read(rs, i + 1);
+ 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);
+ }
+ }
+
+ <Y, X> void appendSelectList(SQLStatement stat, Query<Y> query, X x) {
+ for (int i = 0; i < fields.size(); i++) {
+ if (i > 0) {
+ stat.appendSQL(", ");
+ }
+ FieldDefinition def = fields.get(i);
+ Object obj = def.getValue(x);
+ query.appendSQL(stat, obj);
+ }
+ }
+
+ <Y, X> void copyAttributeValues(Query<Y> query, X to, X map) {
+ for (FieldDefinition def : fields) {
+ Object obj = def.getValue(map);
+ SelectColumn<Y> col = query.getSelectColumn(obj);
+ Object value = col.getCurrentValue();
+ def.setValue(to, value);
+ }
+ }
+
+}
diff --git a/src/com/iciql/TableInspector.java b/src/com/iciql/TableInspector.java new file mode 100644 index 0000000..f920315 --- /dev/null +++ b/src/com/iciql/TableInspector.java @@ -0,0 +1,674 @@ +/* + * 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.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.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.IndexType; +import com.iciql.Iciql.IQColumn; +import com.iciql.Iciql.IQIndex; +import com.iciql.Iciql.IQSchema; +import com.iciql.Iciql.IQTable; +import com.iciql.TableDefinition.FieldDefinition; +import com.iciql.TableDefinition.IndexDefinition; +import com.iciql.util.StatementBuilder; +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 boolean forceUpperCase; + private Class<? extends java.util.Date> dateTimeClass; + private List<String> primaryKeys = Utils.newArrayList(); + private Map<String, IndexInspector> indexes; + private Map<String, ColumnInspector> columns; + private final String eol = "\n"; + + TableInspector(String schema, String table, boolean forceUpperCase, + Class<? extends java.util.Date> dateTimeClass) { + this.schema = schema; + this.table = table; + this.forceUpperCase = forceUpperCase; + this.dateTimeClass = dateTimeClass; + } + + /** + * Tests to see if this TableInspector represents schema.table. + * <p> + * + * @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) && info.name.toLowerCase().startsWith("primary")) { + // 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.allowNull = rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable; + col.isAutoIncrement = rs.getBoolean("IS_AUTOINCREMENT"); + 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, col); + } + } finally { + closeSilently(rs); + } + } + + /** + * Generates a model (class definition) from this table. The model includes + * indexes, primary keys, default values, maxLengths, and allowNull + * information. + * <p> + * 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. + * <p> + * + * @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<String> imports = Utils.newHashSet(); + imports.add(IQSchema.class.getCanonicalName()); + imports.add(IQTable.class.getCanonicalName()); + imports.add(IQIndex.class.getCanonicalName()); + imports.add(IQColumn.class.getCanonicalName()); + + // fields + StringBuilder fields = new StringBuilder(); + List<ColumnInspector> 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<String> sortedImports = new ArrayList<String>(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("name", 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) { + StringBuilder pk = new StringBuilder(); + for (String key : primaryKeys) { + pk.append(key).append(' '); + } + pk.trimToSize(); + ap.addParameter("primaryKey", pk.toString()); + } + + // finish @IQTable annotation + model.append(ap); + model.append(')').append(eol); + + // @IQIndex + ap = new AnnotationBuilder(); + generateIndexAnnotations(ap, "standard", IndexType.STANDARD); + generateIndexAnnotations(ap, "unique", IndexType.UNIQUE); + generateIndexAnnotations(ap, "hash", IndexType.HASH); + generateIndexAnnotations(ap, "uniqueHash", IndexType.UNIQUE_HASH); + if (ap.length() > 0) { + model.append('@').append(IQIndex.class.getSimpleName()); + model.append('('); + model.append(ap); + model.append(')').append(eol); + } + + // class declaration + String clazzName = ModelUtils.convertTableToClassName(table); + model.append(format("public class {0} '{'", clazzName)).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 + */ + void generateIndexAnnotations(AnnotationBuilder ap, String parameter, IndexType type) { + List<IndexInspector> list = getIndexes(type); + if (list.size() == 0) { + // no matching indexes + return; + } + if (list.size() == 1) { + ap.addParameter(parameter, list.get(0).getColumnsString()); + } else { + List<String> parameters = Utils.newArrayList(); + for (IndexInspector index : list) { + parameters.add(index.getColumnsString()); + } + ap.addParameter(parameter, parameters); + } + + } + + private List<IndexInspector> getIndexes(IndexType type) { + List<IndexInspector> list = Utils.newArrayList(); + for (IndexInspector index : indexes.values()) { + if (index.type.equals(type)) { + list.add(index); + } + } + return list; + } + + private StatementBuilder generateColumn(Set<String> 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 { + // @IQColumn + imports.add(clazz.getCanonicalName()); + 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.maxLength + if ((clazz == String.class) && (col.size > 0) && (col.size < Integer.MAX_VALUE)) { + ap.addParameter("maxLength", col.size); + + // IQColumn.trimStrings + if (trimStrings) { + ap.addParameter("trimString=true"); + } + } else { + // IQColumn.AutoIncrement + if (col.isAutoIncrement) { + ap.addParameter("autoIncrement=true"); + } + } + + // IQColumn.allowNull + if (!col.allowNull) { + ap.addParameter("allowNull=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 + */ + <T> List<ValidationRemark> validate(TableDefinition<T> def, boolean throwError) { + List<ValidationRemark> 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}(name={1})", IQSchema.class.getSimpleName(), schema))); + } else if (!schema.equalsIgnoreCase(def.schemaName)) { + remarks.add(error( + table, + "SCHEMA", + format("@{0}(name={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 <T> void validate(List<ValidationRemark> remarks, TableDefinition<T> def, IndexInspector index, + boolean throwError) { + List<IndexDefinition> defIndexes = def.getIndexes(IndexType.STANDARD); + List<IndexInspector> dbIndexes = getIndexes(IndexType.STANDARD); + if (defIndexes.size() > dbIndexes.size()) { + remarks.add(warn(table, IndexType.STANDARD.name(), "More model indexes than database indexes")); + } else if (defIndexes.size() < dbIndexes.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. + } + + /** + * 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<ValidationRemark> remarks, FieldDefinition fieldDef, boolean throwError) { + // unknown field + String field = forceUpperCase ? fieldDef.columnName.toUpperCase() : fieldDef.columnName; + if (!columns.containsKey(field)) { + // unknown column mapping + remarks.add(error(table, fieldDef, "Does not exist in database!").throwError(throwError)); + return; + } + ColumnInspector col = columns.get(field); + 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.maxLength != col.size) && (col.size < Integer.MAX_VALUE)) { + remarks.add(warn( + table, + col, + format("{0}.maxLength={1}, ColumnMaxLength={2}", IQColumn.class.getSimpleName(), + fieldDef.maxLength, col.size))); + } + if (fieldDef.maxLength > 0 && !fieldDef.trimString) { + remarks.add(consider(table, col, + format("{0}.truncateToMaxLength=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}.isAutoIncrement={1}" + " while Column autoIncrement={2}", + IQColumn.class.getSimpleName(), fieldDef.isAutoIncrement, col.isAutoIncrement))); + } + // default value + if (!col.isAutoIncrement && !col.isPrimaryKey) { + // check Model.defaultValue format + if (!ModelUtils.isProperlyFormattedDefaultValue(fieldDef.defaultValue)) { + remarks.add(error( + table, + col, + format("{0}.defaultValue=\"{1}\"" + " is improperly formatted!", + IQColumn.class.getSimpleName(), fieldDef.defaultValue)) + .throwError(throwError)); + // next field + return; + } + // compare Model.defaultValue to Column.defaultValue + if (isNullOrEmpty(fieldDef.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(fieldDef.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(), fieldDef.defaultValue))); + } else if (!isNullOrEmpty(fieldDef.defaultValue) && !isNullOrEmpty(col.defaultValue)) { + if (!fieldDef.defaultValue.equals(col.defaultValue)) { + // Model.defaultValue != Column.defaultValue + remarks.add(warn( + table, + col, + format("{0}.defaultValue=\"{1}\"" + " while column default=\"{2}\"", + IQColumn.class.getSimpleName(), fieldDef.defaultValue, col.defaultValue))); + } + } + + // sanity check Model.defaultValue literal value + if (!ModelUtils.isValidDefaultValue(fieldDef.field.getType(), fieldDef.defaultValue)) { + remarks.add(error( + table, + col, + format("{0}.defaultValue=\"{1}\" is invalid!", IQColumn.class.getSimpleName(), + fieldDef.defaultValue))); + } + } + } + + /** + * Represents an index as it exists in the database. + */ + private static class IndexInspector { + + String name; + IndexType type; + private List<String> columns = new ArrayList<String>(); + + 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")); + } + + public String getColumnsString() { + StatementBuilder sb = new StatementBuilder(); + for (String col : columns) { + sb.appendExceptFirst(", "); + sb.append(col); + } + return sb.toString().trim(); + } + } + + /** + * Represents a column as it exists in the database. + */ + static class ColumnInspector implements Comparable<ColumnInspector> { + String name; + String type; + int size; + boolean allowNull; + 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); + } + + <T> void addParameter(String parameter, T value) { + appendExceptFirst(", "); + 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('\"'); + } + } + } + } +}
\ No newline at end of file diff --git a/src/com/iciql/TestCondition.java b/src/com/iciql/TestCondition.java new file mode 100644 index 0000000..b4c1c15 --- /dev/null +++ b/src/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 <A>
+ * the incomplete condition data type
+ */
+
+public class TestCondition<A> {
+
+ 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 <T> void appendSQL(SQLStatement stat, Query<T> query) {
+ stat.appendSQL("(");
+ query.appendSQL(stat, x[0]);
+ stat.appendSQL(" = ");
+ query.appendSQL(stat, x[1]);
+ stat.appendSQL(")");
+ }
+ });
+ }
+
+ public Boolean bigger(A y) {
+ Boolean o = Utils.newObject(Boolean.class);
+ return Db.registerToken(o, new Function(">", x, y) {
+ public <T> void appendSQL(SQLStatement stat, Query<T> query) {
+ stat.appendSQL("(");
+ query.appendSQL(stat, x[0]);
+ stat.appendSQL(" > ");
+ query.appendSQL(stat, x[1]);
+ stat.appendSQL(")");
+ }
+ });
+ }
+
+ public Boolean biggerEqual(A y) {
+ Boolean o = Utils.newObject(Boolean.class);
+ return Db.registerToken(o, new Function(">=", x, y) {
+ public <T> void appendSQL(SQLStatement stat, Query<T> query) {
+ stat.appendSQL("(");
+ query.appendSQL(stat, x[0]);
+ stat.appendSQL(" >= ");
+ query.appendSQL(stat, x[1]);
+ stat.appendSQL(")");
+ }
+ });
+ }
+
+ public Boolean smaller(A y) {
+ Boolean o = Utils.newObject(Boolean.class);
+ return Db.registerToken(o, new Function("<", x, y) {
+ public <T> void appendSQL(SQLStatement stat, Query<T> query) {
+ stat.appendSQL("(");
+ query.appendSQL(stat, x[0]);
+ stat.appendSQL(" < ");
+ query.appendSQL(stat, x[1]);
+ stat.appendSQL(")");
+ }
+ });
+ }
+
+ public Boolean smallerEqual(A y) {
+ Boolean o = Utils.newObject(Boolean.class);
+ return Db.registerToken(o, new Function("<=", x, y) {
+ public <T> void appendSQL(SQLStatement stat, Query<T> query) {
+ stat.appendSQL("(");
+ query.appendSQL(stat, x[0]);
+ stat.appendSQL(" <= ");
+ query.appendSQL(stat, 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 <T> void appendSQL(SQLStatement stat, Query<T> query) {
+ stat.appendSQL("(");
+ query.appendSQL(stat, x[0]);
+ stat.appendSQL(" LIKE ");
+ query.appendSQL(stat, x[1]);
+ stat.appendSQL(")");
+ }
+ });
+ }
+
+}
diff --git a/src/com/iciql/Token.java b/src/com/iciql/Token.java new file mode 100644 index 0000000..cc2203c --- /dev/null +++ b/src/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
+ */
+
+ <T> void appendSQL(SQLStatement stat, Query<T> query);
+
+}
diff --git a/src/com/iciql/UpdateColumn.java b/src/com/iciql/UpdateColumn.java new file mode 100644 index 0000000..1eaf14c --- /dev/null +++ b/src/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/com/iciql/UpdateColumnIncrement.java b/src/com/iciql/UpdateColumnIncrement.java new file mode 100644 index 0000000..43ef6d6 --- /dev/null +++ b/src/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 <T> + * the query type + * @param <A> + * the new value data type + */ + +public class UpdateColumnIncrement<T, A> implements UpdateColumn { + + private Query<T> query; + private A x; + private A y; + + UpdateColumnIncrement(Query<T> query, A x) { + this.query = query; + this.x = x; + } + + public Query<T> by(A y) { + query.addUpdateColumnDeclaration(this); + this.y = y; + return query; + } + + public void appendSQL(SQLStatement stat) { + query.appendSQL(stat, x); + stat.appendSQL("=("); + query.appendSQL(stat, x); + stat.appendSQL("+"); + query.appendSQL(stat, y); + stat.appendSQL(")"); + } + +} diff --git a/src/com/iciql/UpdateColumnSet.java b/src/com/iciql/UpdateColumnSet.java new file mode 100644 index 0000000..55d3790 --- /dev/null +++ b/src/com/iciql/UpdateColumnSet.java @@ -0,0 +1,52 @@ +/* + * 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 <T> + * the query type + * @param <A> + * the new value data type + */ + +public class UpdateColumnSet<T, A> implements UpdateColumn { + + private Query<T> query; + private A x; + private A y; + + UpdateColumnSet(Query<T> query, A x) { + this.query = query; + this.x = x; + } + + public Query<T> to(A y) { + query.addUpdateColumnDeclaration(this); + this.y = y; + return query; + } + + public void appendSQL(SQLStatement stat) { + query.appendSQL(stat, x); + stat.appendSQL("=?"); + stat.addParameter(y); + } + +} diff --git a/src/com/iciql/ValidationRemark.java b/src/com/iciql/ValidationRemark.java new file mode 100644 index 0000000..a68bf21 --- /dev/null +++ b/src/com/iciql/ValidationRemark.java @@ -0,0 +1,131 @@ +/* + * 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; + } + + private Level level; + private String table; + private String fieldType; + private String fieldName; + private 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 Level getLevel() { + return level; + } + + 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/com/iciql/build/Build.java b/src/com/iciql/build/Build.java new file mode 100644 index 0000000..5963d16 --- /dev/null +++ b/src/com/iciql/build/Build.java @@ -0,0 +1,234 @@ +/*
+ * 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.build;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.iciql.util.StringUtils;
+
+/**
+ * The Build class downloads runtime and compile-time jar files from the Apache
+ * Maven repositories.
+ *
+ * Its important that this class have minimal compile dependencies since its
+ * called very early in the build script.
+ *
+ */
+public class Build {
+
+ /**
+ * BuildTypes
+ */
+ public static enum BuildType {
+ RUNTIME, COMPILETIME;
+ }
+
+ public static void main(String... args) {
+ runtime();
+ compiletime();
+ }
+
+ public static void runtime() {
+ }
+
+ public static void compiletime() {
+ downloadFromApache(MavenObject.H2, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.H2, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.JCOMMANDER, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.JCOMMANDER, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.MARKDOWNPAPERS, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.MARKDOWNPAPERS, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.JUNIT, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.DOCLAVA, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.DOCLAVA, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.SLF4JAPI, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.SLF4JAPI, BuildType.COMPILETIME);
+
+ // needed for site publishing
+ downloadFromApache(MavenObject.COMMONSNET, BuildType.RUNTIME);
+ }
+
+ /**
+ * Download a file from the official Apache Maven repository.
+ *
+ * @param mo
+ * the maven object to download.
+ * @return
+ */
+ private static List<File> downloadFromApache(MavenObject mo, BuildType type) {
+ return downloadFromMaven("http://repo1.maven.org/maven2/", mo, type);
+ }
+
+ /**
+ * Download a file from a Maven repository.
+ *
+ * @param mo
+ * the maven object to download.
+ * @return
+ */
+ private static List<File> downloadFromMaven(String mavenRoot, MavenObject mo, BuildType type) {
+ List<File> downloads = new ArrayList<File>();
+ String[] jars = { "" };
+ if (BuildType.RUNTIME.equals(type)) {
+ jars = new String[] { "" };
+ } else if (BuildType.COMPILETIME.equals(type)) {
+ jars = new String[] { "-sources", "-javadoc" };
+ }
+ for (String jar : jars) {
+ File targetFile = mo.getLocalFile("ext", jar);
+ if (targetFile.exists()) {
+ downloads.add(targetFile);
+ continue;
+ }
+ String expectedSHA1 = mo.getSHA1(jar);
+ if (expectedSHA1 == null) {
+ // skip this jar
+ continue;
+ }
+ String mavenURL = mavenRoot + mo.getRepositoryPath(jar);
+ if (!targetFile.getAbsoluteFile().getParentFile().exists()) {
+ boolean success = targetFile.getAbsoluteFile().getParentFile().mkdirs();
+ if (!success) {
+ throw new RuntimeException("Failed to create destination folder structure!");
+ }
+ }
+ ByteArrayOutputStream buff = new ByteArrayOutputStream();
+ try {
+ URL url = new URL(mavenURL);
+ InputStream in = new BufferedInputStream(url.openStream());
+ byte[] buffer = new byte[4096];
+
+ System.out.println("d/l: " + targetFile.getName());
+ while (true) {
+ int len = in.read(buffer);
+ if (len < 0) {
+ break;
+ }
+ buff.write(buffer, 0, len);
+ }
+ in.close();
+
+ } catch (IOException e) {
+ throw new RuntimeException("Error downloading " + mavenURL + " to " + targetFile, e);
+ }
+ byte[] data = buff.toByteArray();
+ String calculatedSHA1 = StringUtils.calculateSHA1(data);
+
+ System.out.println();
+
+ if (expectedSHA1.length() == 0) {
+ System.out.println("sha: " + calculatedSHA1);
+ System.out.println();
+ } else {
+ if (!calculatedSHA1.equals(expectedSHA1)) {
+ throw new RuntimeException("SHA1 checksum mismatch; got: " + calculatedSHA1);
+ }
+ }
+ try {
+ RandomAccessFile ra = new RandomAccessFile(targetFile, "rw");
+ ra.write(data);
+ ra.setLength(data.length);
+ ra.close();
+ } catch (IOException e) {
+ throw new RuntimeException("Error writing to file " + targetFile, e);
+ }
+ downloads.add(targetFile);
+ }
+ return downloads;
+ }
+
+ /**
+ * Class that describes a retrievable Maven object.
+ */
+ private static class MavenObject {
+
+ public static final MavenObject JCOMMANDER = new MavenObject("com/beust", "jcommander", "1.17",
+ "219a3540f3b27d7cc3b1d91d6ea046cd8723290e", "0bb50eec177acf0e94d58e0cf07262fe5164331d",
+ "c7adc475ca40c288c93054e0f4fe58f3a98c0cb5");
+
+ public static final MavenObject H2 = new MavenObject("com/h2database", "h2", "1.3.158",
+ "4bac13427caeb32ef6e93b70101e61f370c7b5e2", "6bb165156a0831879fa7797df6e18bdcd4421f2d",
+ "446d3f58c44992534cb54f67134532d95961904a");
+
+ public static final MavenObject JUNIT = new MavenObject("junit", "junit", "4.8.2",
+ "c94f54227b08100974c36170dcb53329435fe5ad", "", "");
+
+ public static final MavenObject MARKDOWNPAPERS = new MavenObject("org/tautua/markdownpapers",
+ "markdownpapers-core", "1.1.0", "b879b4720fa642d3c490ab559af132daaa16dbb4",
+ "d98c53939815be2777d5a56dcdc3bbc9ddb468fa", "4c09d2d3073e85b973572292af00bd69681df76b");
+
+ public static final MavenObject COMMONSNET = new MavenObject("commons-net", "commons-net", "1.4.0",
+ "eb47e8cad2dd7f92fd7e77df1d1529cae87361f7", "", "");
+
+ public static final MavenObject DOCLAVA = new MavenObject("com/google/doclava", "doclava", "1.0.3",
+ "5a1e05977fd36480b0cf314410440f88e3a0049e", "6e314df1733455d66b98b56014363172773d0905",
+ "1c1aa631b235439356e6e5803319caca80aaaa88");
+
+ public static final MavenObject SLF4JAPI = new MavenObject("org/slf4j", "slf4j-api", "1.6.1",
+ "6f3b8a24bf970f17289b234284c94f43eb42f0e4", "46a386136c901748e6a3af67ebde6c22bc6b4524",
+ "e223571d77769cdafde59040da235842f3326453");
+
+ public final String group;
+ public final String artifact;
+ public final String version;
+ public final String librarySHA1;
+ public final String sourcesSHA1;
+ public final String javadocSHA1;
+
+ private MavenObject(String group, String artifact, String version, String librarySHA1,
+ String sourcesSHA1, String javadocSHA1) {
+ this.group = group;
+ this.artifact = artifact;
+ this.version = version;
+ this.librarySHA1 = librarySHA1;
+ this.sourcesSHA1 = sourcesSHA1;
+ this.javadocSHA1 = javadocSHA1;
+ }
+
+ private String getRepositoryPath(String jar) {
+ return group + "/" + artifact + "/" + version + "/" + artifact + "-" + version + jar + ".jar";
+ }
+
+ private File getLocalFile(String basePath, String jar) {
+ return new File(basePath, artifact + "-" + version + jar + ".jar");
+ }
+
+ private String getSHA1(String jar) {
+ if (jar.equals("")) {
+ return librarySHA1;
+ } else if (jar.equals("-sources")) {
+ return sourcesSHA1;
+ } else if (jar.equals("-javadoc")) {
+ return javadocSHA1;
+ }
+ return librarySHA1;
+ }
+
+ @Override
+ public String toString() {
+ return group;
+ }
+ }
+}
diff --git a/src/com/iciql/build/BuildSite.java b/src/com/iciql/build/BuildSite.java new file mode 100644 index 0000000..0686def --- /dev/null +++ b/src/com/iciql/build/BuildSite.java @@ -0,0 +1,320 @@ +/*
+ * 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.build;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.nio.charset.Charset;
+import java.text.MessageFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+import org.tautua.markdownpapers.Markdown;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import com.beust.jcommander.Parameters;
+import com.iciql.Constants;
+import com.iciql.util.StringUtils;
+
+/**
+ * Builds the web site or deployment documentation from Markdown source files.
+ *
+ * All Markdown source files must have the .mkd extension.
+ *
+ * Natural string sort order of the Markdown source filenames is the order of
+ * page links. "##_" prefixes are used to control the sort order.
+ *
+ * @author James Moger
+ *
+ */
+public class BuildSite {
+
+ public static void main(String... args) {
+ Params params = new Params();
+ JCommander jc = new JCommander(params);
+ try {
+ jc.parse(args);
+ } catch (ParameterException t) {
+ usage(jc, t);
+ }
+
+ File sourceFolder = new File(params.sourceFolder);
+ File destinationFolder = new File(params.outputFolder);
+ File[] markdownFiles = sourceFolder.listFiles(new FilenameFilter() {
+
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.toLowerCase().endsWith(".mkd");
+ }
+ });
+ Arrays.sort(markdownFiles);
+
+ Map<String, String> aliasMap = new HashMap<String, String>();
+ for (String alias : params.aliases) {
+ String[] values = alias.split("=");
+ aliasMap.put(values[0], values[1]);
+ }
+
+ System.out.println(MessageFormat.format("Generating site from {0} Markdown Docs in {1} ", markdownFiles.length,
+ sourceFolder.getAbsolutePath()));
+ String linkPattern = "<a href=''{0}''>{1}</a>";
+ StringBuilder sb = new StringBuilder();
+ for (File file : markdownFiles) {
+ String documentName = getDocumentName(file);
+ if (!params.skips.contains(documentName)) {
+ String displayName = documentName;
+ if (aliasMap.containsKey(documentName)) {
+ displayName = aliasMap.get(documentName);
+ } else {
+ displayName = displayName.replace('_', ' ');
+ }
+ String fileName = documentName + ".html";
+ sb.append(MessageFormat.format(linkPattern, fileName, displayName));
+ sb.append(" | ");
+ }
+ }
+ sb.setLength(sb.length() - 3);
+ sb.trimToSize();
+
+ String htmlHeader = readContent(new File(params.pageHeader), "\n");
+
+ String htmlAdSnippet = null;
+ if (!StringUtils.isNullOrEmpty(params.adSnippet)) {
+ File snippet = new File(params.adSnippet);
+ if (snippet.exists()) {
+ htmlAdSnippet = readContent(snippet, "\n");
+ }
+ }
+ String htmlFooter = readContent(new File(params.pageFooter), "\n");
+ String links = sb.toString();
+ String header = MessageFormat.format(htmlHeader, Constants.NAME, links);
+ if (!StringUtils.isNullOrEmpty(params.analyticsSnippet)) {
+ File snippet = new File(params.analyticsSnippet);
+ if (snippet.exists()) {
+ String htmlSnippet = readContent(snippet, "\n");
+ header = header.replace("<!-- ANALYTICS -->", htmlSnippet);
+ }
+ }
+ final String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
+ final String footer = MessageFormat.format(htmlFooter, "generated " + date);
+ for (File file : markdownFiles) {
+ try {
+ String documentName = getDocumentName(file);
+ if (!params.skips.contains(documentName)) {
+ String fileName = documentName + ".html";
+ System.out.println(MessageFormat.format(" {0} => {1}", file.getName(), fileName));
+ String rawContent = readContent(file, "\n");
+ String markdownContent = rawContent;
+
+ Map<String, List<String>> nomarkdownMap = new HashMap<String, List<String>>();
+
+ // extract sections marked as no-markdown
+ int nmd = 0;
+ for (String token : params.nomarkdown) {
+ StringBuilder strippedContent = new StringBuilder();
+
+ String nomarkdownKey = "%NOMARKDOWN" + nmd + "%";
+ String[] kv = token.split(":", 2);
+ String beginToken = kv[0];
+ String endToken = kv[1];
+
+ // strip nomarkdown chunks from markdown and cache them
+ List<String> chunks = new Vector<String>();
+ int beginCode = 0;
+ int endCode = 0;
+ while ((beginCode = markdownContent.indexOf(beginToken, endCode)) > -1) {
+ if (endCode == 0) {
+ strippedContent.append(markdownContent.substring(0, beginCode));
+ } else {
+ strippedContent.append(markdownContent.substring(endCode, beginCode));
+ }
+ strippedContent.append(nomarkdownKey);
+ endCode = markdownContent.indexOf(endToken, beginCode);
+ chunks.add(markdownContent.substring(beginCode, endCode));
+ nomarkdownMap.put(nomarkdownKey, chunks);
+ }
+
+ // get remainder of text
+ if (endCode < markdownContent.length()) {
+ strippedContent.append(markdownContent.substring(endCode, markdownContent.length()));
+ }
+ markdownContent = strippedContent.toString();
+ nmd++;
+ }
+
+ // transform markdown to html
+ String content = transformMarkdown(new StringReader(markdownContent.toString()));
+
+ // reinsert nomarkdown chunks
+ for (Map.Entry<String, List<String>> nomarkdown : nomarkdownMap.entrySet()) {
+ for (String chunk : nomarkdown.getValue()) {
+ content = content.replaceFirst(nomarkdown.getKey(), chunk);
+ }
+ }
+
+ // perform specified substitutions
+ for (String token : params.substitutions) {
+ String[] kv = token.split("=", 2);
+ content = content.replace(kv[0], kv[1]);
+ }
+
+ OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(new File(destinationFolder,
+ fileName)), Charset.forName("UTF-8"));
+ writer.write(header);
+ if (!StringUtils.isNullOrEmpty(htmlAdSnippet)) {
+ writer.write(htmlAdSnippet);
+ }
+ writer.write(content);
+ writer.write(footer);
+ writer.close();
+ }
+ } catch (Throwable t) {
+ System.err.println("Failed to transform " + file.getName());
+ t.printStackTrace();
+ }
+ }
+ }
+
+ private static String getDocumentName(File file) {
+ String displayName = file.getName().substring(0, file.getName().lastIndexOf('.')).toLowerCase();
+ int underscore = displayName.indexOf('_') + 1;
+ if (underscore > -1) {
+ // trim leading ##_ which is to control display order
+ return displayName.substring(underscore);
+ }
+ return displayName;
+ }
+
+ /**
+ * Returns the string content of the specified file.
+ *
+ * @param file
+ * @param lineEnding
+ * @return the string content of the file
+ */
+ private 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();
+ }
+
+ private static String transformMarkdown(Reader markdownReader) throws ParseException {
+ // Read raw markdown content and transform it to html
+ StringWriter writer = new StringWriter();
+ try {
+ Markdown md = new Markdown();
+ md.transform(markdownReader, writer);
+ return writer.toString().trim();
+ } catch (org.tautua.markdownpapers.parser.ParseException p) {
+ throw new java.text.ParseException(p.getMessage(), 0);
+ } finally {
+ try {
+ markdownReader.close();
+ } catch (IOException e) {
+ // IGNORE
+ }
+ try {
+ writer.close();
+ } catch (IOException e) {
+ // IGNORE
+ }
+ }
+ }
+
+ private static void usage(JCommander jc, ParameterException t) {
+ System.out.println(Constants.NAME + " v" + Constants.VERSION);
+ System.out.println();
+ if (t != null) {
+ System.out.println(t.getMessage());
+ System.out.println();
+ }
+ if (jc != null) {
+ jc.usage();
+ }
+ System.exit(0);
+ }
+
+ /**
+ * Command-line parameters for BuildSite utility.
+ */
+ @Parameters(separators = " ")
+ private static class Params {
+
+ @Parameter(names = { "--sourceFolder" }, description = "Markdown Source Folder", required = true)
+ public String sourceFolder;
+
+ @Parameter(names = { "--outputFolder" }, description = "HTML Ouptut Folder", required = true)
+ public String outputFolder;
+
+ @Parameter(names = { "--pageHeader" }, description = "Page Header HTML Snippet", required = true)
+ public String pageHeader;
+
+ @Parameter(names = { "--pageFooter" }, description = "Page Footer HTML Snippet", required = true)
+ public String pageFooter;
+
+ @Parameter(names = { "--adSnippet" }, description = "Ad HTML Snippet", required = false)
+ public String adSnippet;
+
+ @Parameter(names = { "--analyticsSnippet" }, description = "Analytics HTML Snippet", required = false)
+ public String analyticsSnippet;
+
+ @Parameter(names = { "--skip" }, description = "Filename to skip", required = false)
+ public List<String> skips = new ArrayList<String>();
+
+ @Parameter(names = { "--alias" }, description = "Filename=Linkname aliases", required = false)
+ public List<String> aliases = new ArrayList<String>();
+
+ @Parameter(names = { "--substitute" }, description = "%TOKEN%=value", required = false)
+ public List<String> substitutions = new ArrayList<String>();
+
+ @Parameter(names = { "--nomarkdown" }, description = "%STARTTOKEN%:%ENDTOKEN%", required = false)
+ public List<String> nomarkdown = new ArrayList<String>();
+
+ }
+}
diff --git a/src/com/iciql/bytecode/And.java b/src/com/iciql/bytecode/And.java new file mode 100644 index 0000000..808a9c1 --- /dev/null +++ b/src/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 <T> void appendSQL(SQLStatement stat, Query<T> query) { + left.appendSQL(stat, query); + stat.appendSQL(" AND "); + right.appendSQL(stat, query); + } + +} diff --git a/src/com/iciql/bytecode/ArrayGet.java b/src/com/iciql/bytecode/ArrayGet.java new file mode 100644 index 0000000..29516c2 --- /dev/null +++ b/src/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 <T> void appendSQL(SQLStatement stat, Query<T> query) { + // untested + variable.appendSQL(stat, query); + stat.appendSQL("["); + index.appendSQL(stat, query); + stat.appendSQL("]"); + } + +} diff --git a/src/com/iciql/bytecode/CaseWhen.java b/src/com/iciql/bytecode/CaseWhen.java new file mode 100644 index 0000000..2a1d69e --- /dev/null +++ b/src/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 <T> void appendSQL(SQLStatement stat, Query<T> 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/com/iciql/bytecode/ClassReader.java b/src/com/iciql/bytecode/ClassReader.java new file mode 100644 index 0000000..35e94e9 --- /dev/null +++ b/src/com/iciql/bytecode/ClassReader.java @@ -0,0 +1,1454 @@ +/* + * 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<Token> stack = new Stack<Token>(); + private ArrayList<Token> variables = new ArrayList<Token>(); + private boolean endOfMethod; + private boolean condition; + private int nextPc; + private Map<String, Object> fieldMap = new HashMap<String, Object>(); + + private static void debug(String s) { + if (DEBUG) { + System.out.println(s); + } + } + + public Token decompile(Object instance, Map<String, Object> 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<Token> currentStack = new Stack<Token>(); + currentStack.addAll(stack); + ArrayList<Token> currentVariables = new ArrayList<Token>(); + 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/com/iciql/bytecode/Constant.java b/src/com/iciql/bytecode/Constant.java new file mode 100644 index 0000000..65cd66b --- /dev/null +++ b/src/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/com/iciql/bytecode/ConstantNumber.java b/src/com/iciql/bytecode/ConstantNumber.java new file mode 100644 index 0000000..934de3d --- /dev/null +++ b/src/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 <T> void appendSQL(SQLStatement stat, Query<T> query) { + stat.appendSQL(toString()); + } + + public Constant.Type getType() { + return type; + } + +} diff --git a/src/com/iciql/bytecode/ConstantString.java b/src/com/iciql/bytecode/ConstantString.java new file mode 100644 index 0000000..985f97d --- /dev/null +++ b/src/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 <T> void appendSQL(SQLStatement stat, Query<T> query) { + stat.appendSQL(StringUtils.quoteStringSQL(value)); + } + + public Constant.Type getType() { + return Constant.Type.STRING; + } + +} diff --git a/src/com/iciql/bytecode/Function.java b/src/com/iciql/bytecode/Function.java new file mode 100644 index 0000000..56a55ea --- /dev/null +++ b/src/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 <T> void appendSQL(SQLStatement stat, Query<T> query) { + // untested + stat.appendSQL(name + "("); + expr.appendSQL(stat, query); + stat.appendSQL(")"); + } +} diff --git a/src/com/iciql/bytecode/Not.java b/src/com/iciql/bytecode/Not.java new file mode 100644 index 0000000..ab5ab84 --- /dev/null +++ b/src/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 <T> void appendSQL(SQLStatement stat, Query<T> query) { + // untested + stat.appendSQL("NOT("); + expr.appendSQL(stat, query); + stat.appendSQL(")"); + } + +} diff --git a/src/com/iciql/bytecode/Null.java b/src/com/iciql/bytecode/Null.java new file mode 100644 index 0000000..a28de56 --- /dev/null +++ b/src/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 <T> void appendSQL(SQLStatement stat, Query<T> query) { + // untested + stat.appendSQL("NULL"); + } + +} diff --git a/src/com/iciql/bytecode/Operation.java b/src/com/iciql/bytecode/Operation.java new file mode 100644 index 0000000..7cd42d9 --- /dev/null +++ b/src/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 <T> void appendSQL(SQLStatement stat, Query<T> query) { + left.appendSQL(stat, query); + stat.appendSQL(op.toString()); + right.appendSQL(stat, query); + } + +} diff --git a/src/com/iciql/bytecode/Or.java b/src/com/iciql/bytecode/Or.java new file mode 100644 index 0000000..37da2a6 --- /dev/null +++ b/src/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 <T> void appendSQL(SQLStatement stat, Query<T> query) { + // untested + left.appendSQL(stat, query); + stat.appendSQL(" OR "); + right.appendSQL(stat, query); + } + +} diff --git a/src/com/iciql/bytecode/Variable.java b/src/com/iciql/bytecode/Variable.java new file mode 100644 index 0000000..3412349 --- /dev/null +++ b/src/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 <T> void appendSQL(SQLStatement stat, Query<T> query) { + query.appendSQL(stat, obj); + } + +} diff --git a/src/com/iciql/bytecode/package.html b/src/com/iciql/bytecode/package.html new file mode 100644 index 0000000..5107481 --- /dev/null +++ b/src/com/iciql/bytecode/package.html @@ -0,0 +1,25 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<!-- + 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. +--> +<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> +<head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> +<title>Javadoc package documentation</title> +</head> +<body> +The class decompiler for natural syntax iciql clauses. +</body> +</html>
\ No newline at end of file diff --git a/src/com/iciql/package.html b/src/com/iciql/package.html new file mode 100644 index 0000000..769837b --- /dev/null +++ b/src/com/iciql/package.html @@ -0,0 +1,25 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<!--
+ 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.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+<head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+<title>Javadoc package documentation</title>
+</head>
+<body>
+<i>iciql</i> (pronounced "icicle") is a Java JDBC SQL statement generator and simple object mapper
+</body>
+</html>
\ No newline at end of file diff --git a/src/com/iciql/util/GenerateModels.java b/src/com/iciql/util/GenerateModels.java new file mode 100644 index 0000000..69ce852 --- /dev/null +++ b/src/com/iciql/util/GenerateModels.java @@ -0,0 +1,192 @@ +/* + * 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.Connection; +import java.sql.DriverManager; +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 { + Connection conn = null; + try { + conn = DriverManager.getConnection(url, user, password); + Db db = Db.open(url, user, password.toCharArray()); + DbInspector inspector = new DbInspector(db); + List<String> 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); + } finally { + JdbcUtils.closeSilently(conn); + } + } + + /** + * 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 <string>"); + out.println(" -password <string>"); + out.println(" -schema <string>"); + out.println(" -table <string>"); + out.println(" -package <string>"); + out.println(" -folder <string>"); + out.println(" -annotateSchema <boolean>"); + out.println(" -trimStrings <boolean>"); + } + +} diff --git a/src/com/iciql/util/JdbcUtils.java b/src/com/iciql/util/JdbcUtils.java new file mode 100644 index 0000000..edf21e8 --- /dev/null +++ b/src/com/iciql/util/JdbcUtils.java @@ -0,0 +1,253 @@ +/* + * 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/com/iciql/util/Slf4jStatementListener.java b/src/com/iciql/util/Slf4jStatementListener.java new file mode 100644 index 0000000..2398ade --- /dev/null +++ b/src/com/iciql/util/Slf4jStatementListener.java @@ -0,0 +1,94 @@ +/*
+ * 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.StatementLogger.StatementListener;
+import com.iciql.util.StatementLogger.StatementType;
+
+/**
+ * Slf4jStatementListener interfaces the iciql statement logger to the SLF4J
+ * logging architecture.
+ *
+ */
+public class Slf4jStatementListener implements StatementListener {
+
+ 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<StatementType, Level> levels;
+
+ public Slf4jStatementListener() {
+ this(Level.TRACE);
+ }
+
+ public Slf4jStatementListener(Level defaultLevel) {
+ this.defaultLevel = defaultLevel;
+ levels = new HashMap<StatementType, Level>();
+ 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 logStatement(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/com/iciql/util/StatementBuilder.java b/src/com/iciql/util/StatementBuilder.java new file mode 100644 index 0000000..c2974ae --- /dev/null +++ b/src/com/iciql/util/StatementBuilder.java @@ -0,0 +1,157 @@ +/* + * 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: + * + * <pre> + * StringBuilder buff = new StringBuilder(); + * for (int i = 0; i < args.length; i++) { + * if (i > 0) { + * buff.append(", "); + * } + * buff.append(args[i]); + * } + * </pre> + * + * to + * + * <pre> + * StatementBuilder buff = new StatementBuilder(); + * for (String s : args) { + * buff.appendExceptFirst(", "); + * buff.append(a); + * } + * </pre> + */ +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; + } + + /** + * 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/com/iciql/util/StatementLogger.java b/src/com/iciql/util/StatementLogger.java new file mode 100644 index 0000000..95d644a --- /dev/null +++ b/src/com/iciql/util/StatementLogger.java @@ -0,0 +1,182 @@ +/* + * 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.util.Set; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Utility class to optionally log generated statements to StatementListeners.<br> + * Statement logging is disabled by default. + * <p> + * This class also tracks the counts for generated statements by major type. + * + */ +public class StatementLogger { + + /** + * Enumeration of the different statement types that are logged. + */ + public enum StatementType { + STAT, TOTAL, CREATE, INSERT, UPDATE, MERGE, DELETE, SELECT; + } + + /** + * Interface that defines a statement listener. + */ + public interface StatementListener { + void logStatement(StatementType type, String statement); + } + + private static final Set<StatementListener> LISTENERS = Utils.newHashSet(); + private static final StatementListener CONSOLE = new StatementListener() { + + @Override + public void logStatement(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(); + + /** + * Activates the Console Logger. + */ + public static void activateConsoleLogger() { + LISTENERS.add(CONSOLE); + } + + /** + * Deactivates the Console Logger. + */ + public static void deactivateConsoleLogger() { + LISTENERS.remove(CONSOLE); + } + + /** + * Registers a listener with the relay. + * + * @param listener + */ + public static void registerListener(StatementListener listener) { + LISTENERS.add(listener); + } + + /** + * Unregisters a listener with the relay. + * + * @param listener + */ + public static void unregisterListener(StatementListener listener) { + LISTENERS.remove(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); + } + + private static void logStatement(StatementType type, String statement) { + for (StatementListener listener : LISTENERS) { + try { + listener.logStatement(type, statement); + } catch (Throwable t) { + } + } + } + + 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 getTotalCount() { + return getCreateCount() + getInsertCount() + getUpdateCount() + getDeleteCount() + getMergeCount() + + getSelectCount(); + } + + public static void logStats() { + logStatement(StatementType.STAT, "iciql Runtime Statistics"); + 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()); + 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/com/iciql/util/StringUtils.java b/src/com/iciql/util/StringUtils.java new file mode 100644 index 0000000..2a8c297 --- /dev/null +++ b/src/com/iciql/util/StringUtils.java @@ -0,0 +1,308 @@ +/* + * 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.UnsupportedEncodingException; +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<String> 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; + } +} diff --git a/src/com/iciql/util/Utils.java b/src/com/iciql/util/Utils.java new file mode 100644 index 0000000..3a600fa --- /dev/null +++ b/src/com/iciql/util/Utils.java @@ -0,0 +1,267 @@ +/*
+ * 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.IOException;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.lang.reflect.Constructor;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+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.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+
+import com.iciql.IciqlException;
+
+/**
+ * Generic utility methods.
+ */
+public class Utils {
+
+ public static final AtomicLong COUNTER = new AtomicLong(0);
+
+ private static final boolean MAKE_ACCESSIBLE = true;
+
+ private static final int BUFFER_BLOCK_SIZE = 4 * 1024;
+
+ @SuppressWarnings("unchecked")
+ public static <X> Class<X> getClass(X x) {
+ return (Class<X>) x.getClass();
+ }
+
+ public static Class<?> loadClass(String className) {
+ try {
+ return Class.forName(className);
+ } catch (Exception e) {
+ throw new IciqlException(e);
+ }
+ }
+
+ public static <T> ArrayList<T> newArrayList() {
+ return new ArrayList<T>();
+ }
+
+ public static <T> ArrayList<T> newArrayList(Collection<T> c) {
+ return new ArrayList<T>(c);
+ }
+
+ public static <T> HashSet<T> newHashSet() {
+ return new HashSet<T>();
+ }
+
+ public static <T> HashSet<T> newHashSet(Collection<T> list) {
+ return new HashSet<T>(list);
+ }
+
+ public static <A, B> HashMap<A, B> newHashMap() {
+ return new HashMap<A, B>();
+ }
+
+ public static <A, B> Map<A, B> newSynchronizedHashMap() {
+ HashMap<A, B> map = newHashMap();
+ return Collections.synchronizedMap(map);
+ }
+
+ public static <A, B> IdentityHashMap<A, B> newIdentityHashMap() {
+ return new IdentityHashMap<A, B>();
+ }
+
+ public static <T> ThreadLocal<T> newThreadLocal(final Class<? extends T> clazz) {
+ return new ThreadLocal<T>() {
+ @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("Exception trying to create " + clazz.getName() + ": " + e, e);
+ }
+ }
+ };
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public static <T> T newObject(Class<T> clazz) {
+ // must create new instances
+ if (clazz == Integer.class) {
+ return (T) new Integer((int) COUNTER.getAndIncrement());
+ } else if (clazz == String.class) {
+ return (T) ("" + COUNTER.getAndIncrement());
+ } else if (clazz == Long.class) {
+ return (T) new Long(COUNTER.getAndIncrement());
+ } else if (clazz == Short.class) {
+ return (T) new Short((short) COUNTER.getAndIncrement());
+ } else if (clazz == Byte.class) {
+ return (T) new Byte((byte) COUNTER.getAndIncrement());
+ } else if (clazz == Float.class) {
+ return (T) new Float(COUNTER.getAndIncrement());
+ } else if (clazz == Double.class) {
+ return (T) new Double(COUNTER.getAndIncrement());
+ } else if (clazz == Boolean.class) {
+ 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 == List.class) {
+ return (T) new ArrayList();
+ }
+ 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("Exception trying to create " + clazz.getName() + ": " + e, e);
+ }
+ }
+
+ public static <T> boolean isSimpleType(Class<T> 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;
+ }
+ 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("Error converting CLOB to String: " + e.toString(), e);
+ }
+ }
+ return o.toString();
+ }
+ if (Number.class.isAssignableFrom(currentType)) {
+ Number n = (Number) o;
+ if (targetType == Byte.class) {
+ return n.byteValue();
+ } else if (targetType == Short.class) {
+ return n.shortValue();
+ } else if (targetType == Integer.class) {
+ return n.intValue();
+ } else if (targetType == Long.class) {
+ return n.longValue();
+ } else if (targetType == Double.class) {
+ return n.doubleValue();
+ } else if (targetType == Float.class) {
+ return n.floatValue();
+ }
+ }
+ throw new IciqlException("Can not convert the value " + o + " from " + currentType + " to "
+ + 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();
+ }
+ }
+}
diff --git a/src/com/iciql/util/WeakIdentityHashMap.java b/src/com/iciql/util/WeakIdentityHashMap.java new file mode 100644 index 0000000..bc03cd0 --- /dev/null +++ b/src/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 <K>
+ * the keys
+ * @param <V>
+ * the value
+ */
+
+public class WeakIdentityHashMap<K, V> implements Map<K, V> {
+
+ private static final int MAX_LOAD = 90;
+ private static final WeakReference<Object> DELETED_KEY = new WeakReference<Object>(null);
+ private int mask, len, size, deletedCount, level;
+ private int maxSize, minSize, maxDeleted;
+ private WeakReference<K>[] 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> k = keys[index];
+ if (k == null) {
+ // found an empty record
+ if (deleted >= 0) {
+ index = deleted;
+ deletedCount--;
+ }
+ size++;
+ keys[index] = new WeakReference<K>(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> 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<K>) DELETED_KEY;
+ values[index] = null;
+ deletedCount++;
+ size--;
+ }
+
+ private void rehash(int newLevel) {
+ WeakReference<K>[] oldKeys = keys;
+ V[] oldValues = values;
+ reset(newLevel);
+ for (int i = 0; i < oldKeys.length; i++) {
+ WeakReference<K> 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> 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<java.util.Map.Entry<K, V>> entrySet() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean isEmpty() {
+ return size == 0;
+ }
+
+ public Set<K> keySet() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void putAll(Map<? extends K, ? extends V> m) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Collection<V> values() {
+ throw new UnsupportedOperationException();
+ }
+
+}
diff --git a/src/com/iciql/util/package.html b/src/com/iciql/util/package.html new file mode 100644 index 0000000..3d24dee --- /dev/null +++ b/src/com/iciql/util/package.html @@ -0,0 +1,25 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<!--
+ 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.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+<head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+<title>Javadoc package documentation</title>
+</head>
+<body>
+Utility classes for iciql.
+</body>
+</html>
\ No newline at end of file |