summaryrefslogtreecommitdiffstats
path: root/src/main/java/com/iciql/TableInspector.java
diff options
context:
space:
mode:
authorJames Moger <james.moger@gitblit.com>2016-04-05 12:27:55 -0400
committerJames Moger <james.moger@gitblit.com>2016-04-05 12:27:55 -0400
commitbb87e621eefd97872aa1a619d38c166b2f07db84 (patch)
treed7199be13f0015e218ae2ded4ebd8af11b955845 /src/main/java/com/iciql/TableInspector.java
parent98bb5dc01796728de5b18f84e19766276d12d1db (diff)
downloadiciql-bb87e621eefd97872aa1a619d38c166b2f07db84.tar.gz
iciql-bb87e621eefd97872aa1a619d38c166b2f07db84.zip
Reformat project with default IntelliJ settings
Diffstat (limited to 'src/main/java/com/iciql/TableInspector.java')
-rw-r--r--src/main/java/com/iciql/TableInspector.java1353
1 files changed, 673 insertions, 680 deletions
diff --git a/src/main/java/com/iciql/TableInspector.java b/src/main/java/com/iciql/TableInspector.java
index b717203..d775f0f 100644
--- a/src/main/java/com/iciql/TableInspector.java
+++ b/src/main/java/com/iciql/TableInspector.java
@@ -18,25 +18,6 @@
package com.iciql;
-import static com.iciql.ValidationRemark.consider;
-import static com.iciql.ValidationRemark.error;
-import static com.iciql.ValidationRemark.warn;
-import static com.iciql.util.JdbcUtils.closeSilently;
-import static com.iciql.util.StringUtils.isNullOrEmpty;
-import static java.text.MessageFormat.format;
-
-import java.io.Serializable;
-import java.lang.reflect.Modifier;
-import java.sql.DatabaseMetaData;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
import com.iciql.Iciql.IQColumn;
import com.iciql.Iciql.IQIndex;
import com.iciql.Iciql.IQIndexes;
@@ -51,6 +32,23 @@ import com.iciql.util.StatementBuilder;
import com.iciql.util.StringUtils;
import com.iciql.util.Utils;
+import java.io.Serializable;
+import java.lang.reflect.Modifier;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static com.iciql.ValidationRemark.*;
+import static com.iciql.util.JdbcUtils.closeSilently;
+import static com.iciql.util.StringUtils.isNullOrEmpty;
+import static java.text.MessageFormat.format;
+
/**
* 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
@@ -58,666 +56,661 @@ import com.iciql.util.Utils;
*/
public class TableInspector {
- private String schema;
- private String table;
- 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, Class<? extends java.util.Date> dateTimeClass) {
- this.schema = schema;
- this.table = table;
- 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)) {
- String name = info.name.toLowerCase();
- if (name.startsWith("primary") || name.startsWith("sys_idx_sys_pk")
- || name.startsWith("sql") || name.endsWith("_pkey")) {
- // skip primary key indexes
- continue;
- }
- }
- if (indexes.containsKey(info.name)) {
- indexes.get(info.name).addColumn(rs);
- } else {
- indexes.put(info.name, info);
- }
- }
- closeSilently(rs);
-
- // columns
- rs = metaData.getColumns(null, schema, table, null);
- columns = Utils.newHashMap();
- while (rs.next()) {
- ColumnInspector col = new ColumnInspector();
- col.name = rs.getString("COLUMN_NAME");
- col.type = rs.getString("TYPE_NAME");
- col.clazz = ModelUtils.getClassForSqlType(col.type, dateTimeClass);
- col.size = rs.getInt("COLUMN_SIZE");
- col.nullable = rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable;
- try {
- Object autoIncrement = rs.getObject("IS_AUTOINCREMENT");
- if (autoIncrement instanceof Boolean) {
- col.isAutoIncrement = (Boolean) autoIncrement;
- } else if (autoIncrement instanceof String) {
- String val = autoIncrement.toString().toLowerCase();
- col.isAutoIncrement = val.equals("true") | val.equals("yes");
- } else if (autoIncrement instanceof Number) {
- Number n = (Number) autoIncrement;
- col.isAutoIncrement = n.intValue() > 0;
- }
- } catch (SQLException s) {
+ private String schema;
+ private String table;
+ 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, Class<? extends java.util.Date> dateTimeClass) {
+ this.schema = schema;
+ this.table = table;
+ 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)) {
+ String name = info.name.toLowerCase();
+ if (name.startsWith("primary") || name.startsWith("sys_idx_sys_pk")
+ || name.startsWith("sql") || name.endsWith("_pkey")) {
+ // skip primary key indexes
+ continue;
+ }
+ }
+ if (indexes.containsKey(info.name)) {
+ indexes.get(info.name).addColumn(rs);
+ } else {
+ indexes.put(info.name, info);
+ }
+ }
+ closeSilently(rs);
+
+ // columns
+ rs = metaData.getColumns(null, schema, table, null);
+ columns = Utils.newHashMap();
+ while (rs.next()) {
+ ColumnInspector col = new ColumnInspector();
+ col.name = rs.getString("COLUMN_NAME");
+ col.type = rs.getString("TYPE_NAME");
+ col.clazz = ModelUtils.getClassForSqlType(col.type, dateTimeClass);
+ col.size = rs.getInt("COLUMN_SIZE");
+ col.nullable = rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable;
+ try {
+ Object autoIncrement = rs.getObject("IS_AUTOINCREMENT");
+ if (autoIncrement instanceof Boolean) {
+ col.isAutoIncrement = (Boolean) autoIncrement;
+ } else if (autoIncrement instanceof String) {
+ String val = autoIncrement.toString().toLowerCase();
+ col.isAutoIncrement = val.equals("true") | val.equals("yes");
+ } else if (autoIncrement instanceof Number) {
+ Number n = (Number) autoIncrement;
+ col.isAutoIncrement = n.intValue() > 0;
+ }
+ } catch (SQLException s) {
// throw s;
- }
- if (primaryKeys.size() == 1) {
- if (col.name.equalsIgnoreCase(primaryKeys.get(0))) {
- col.isPrimaryKey = true;
- }
- }
- if (!col.isAutoIncrement) {
- col.defaultValue = rs.getString("COLUMN_DEF");
- }
- columns.put(col.name.toLowerCase(), col);
- }
- } finally {
- closeSilently(rs);
- }
- }
-
- /**
- * Generates a model (class definition) from this table. The model includes
- * indexes, primary keys, default values, lengths, and nullables.
- * information.
- * <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(Serializable.class.getCanonicalName());
- imports.add(IQSchema.class.getCanonicalName());
- imports.add(IQTable.class.getCanonicalName());
- imports.add(IQIndexes.class.getCanonicalName());
- imports.add(IQIndex.class.getCanonicalName());
- imports.add(IQColumn.class.getCanonicalName());
- imports.add(IndexType.class.getCanonicalName());
-
- // fields
- StringBuilder fields = new StringBuilder();
- List<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(null, schema);
- model.append(ap);
- model.append(')').append(eol);
- }
-
- // @IQTable
- model.append('@').append(IQTable.class.getSimpleName());
- model.append('(');
-
- // IQTable annotation parameters
- AnnotationBuilder ap = new AnnotationBuilder();
- ap.addParameter("name", table);
-
- if (primaryKeys.size() > 1) {
- ap.addParameter("primaryKey", primaryKeys);
- }
-
- // finish @IQTable annotation
- model.append(ap);
- model.append(')').append(eol);
-
- // @IQIndexes
- // @IQIndex
- String indexAnnotations = generateIndexAnnotations();
- if (!StringUtils.isNullOrEmpty(indexAnnotations)) {
- model.append(indexAnnotations);
- }
-
- // class declaration
- String clazzName = ModelUtils.convertTableToClassName(table);
- model.append(format("public class {0} implements Serializable '{'", clazzName)).append(eol);
- model.append(eol);
- model.append("\tprivate static final long serialVersionUID = 1L;").append(eol);
- model.append(eol);
-
- // field declarations
- model.append(fields);
-
- // default constructor
- model.append("\t" + "public ").append(clazzName).append("() {").append(eol);
- model.append("\t}").append(eol);
-
- // end of class body
- model.append('}');
- model.trimToSize();
- return model.toString();
- }
-
- /**
- * Generates the specified index annotation.
- *
- * @param ap
- */
- String generateIndexAnnotations() {
- if (indexes == null || indexes.size() == 0) {
- // no matching indexes
- return null;
- }
- AnnotationBuilder ap = new AnnotationBuilder();
- if (indexes.size() == 1) {
- // single index
- IndexInspector index = indexes.values().toArray(new IndexInspector[1])[0];
- ap.append(generateIndexAnnotation(index));
- ap.append(eol);
- } else {
- // multiple indexes
- ap.append('@').append(IQIndexes.class.getSimpleName());
- ap.append("({");
- ap.resetCount();
- for (IndexInspector index : indexes.values()) {
- ap.appendExceptFirst(", ");
- ap.append(generateIndexAnnotation(index));
- }
- ap.append("})").append(eol);
- }
- return ap.toString();
- }
-
- private String generateIndexAnnotation(IndexInspector index) {
- AnnotationBuilder ap = new AnnotationBuilder();
- ap.append('@').append(IQIndex.class.getSimpleName());
- ap.append('(');
- ap.resetCount();
- if (!StringUtils.isNullOrEmpty(index.name)) {
- ap.addParameter("name", index.name);
- }
- if (!index.type.equals(IndexType.STANDARD)) {
- ap.addEnum("type", index.type);
- }
- if (ap.getCount() > 0) {
- // multiple fields specified
- ap.addParameter("value", index.columns);
- } else {
- // default value
- ap.addParameter(null, index.columns);
- }
- ap.append(')');
- return ap.toString();
- }
-
- private StatementBuilder generateColumn(Set<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 {
- // Imports
- // don't import primitives, java.lang classes, or byte []
- if (clazz.getPackage() == null) {
- } else if (clazz.getPackage().getName().equals("java.lang")) {
- } else if (clazz.equals(byte[].class)) {
- } else {
- imports.add(clazz.getCanonicalName());
- }
- // @IQColumn
- sb.append('@').append(IQColumn.class.getSimpleName());
-
- // IQColumn annotation parameters
- AnnotationBuilder ap = new AnnotationBuilder();
-
- // IQColumn.name
- if (!col.name.equalsIgnoreCase(column)) {
- ap.addParameter("name", col.name);
- }
-
- // IQColumn.primaryKey
- // composite primary keys are annotated on the table
- if (col.isPrimaryKey && primaryKeys.size() == 1) {
- ap.addParameter("primaryKey=true");
- }
-
- // IQColumn.length
- if ((clazz == String.class) && (col.size > 0) && (col.size < Integer.MAX_VALUE)) {
- ap.addParameter("length", col.size);
-
- // IQColumn.trim
- if (trimStrings) {
- ap.addParameter("trim=true");
- }
- } else {
- // IQColumn.AutoIncrement
- if (col.isAutoIncrement) {
- ap.addParameter("autoIncrement=true");
- }
- }
-
- // IQColumn.nullable
- if (!col.nullable) {
- ap.addParameter("nullable=false");
- }
-
- // IQColumn.defaultValue
- if (!isNullOrEmpty(col.defaultValue)) {
- ap.addParameter("defaultValue=\"" + col.defaultValue + "\"");
- }
-
- // add leading and trailing ()
- if (ap.length() > 0) {
- ap.insert(0, '(');
- ap.append(')');
- }
- sb.append(ap);
- }
- sb.append(eol);
-
- // variable declaration
- sb.append("\t" + "public ");
- sb.append(clazz.getSimpleName());
- sb.append(' ');
- sb.append(column);
- sb.append(';');
- sb.append(eol).append(eol);
- return sb;
- }
-
- /**
- * Validates that a table definition (annotated, interface, or both) matches
- * the current state of the table and indexes in the database. Results are
- * returned as a list of validation remarks which includes recommendations,
- * warnings, and errors about the model. The caller may choose to have
- * validate throw an exception on any validation ERROR.
- *
- * @param def
- * the table definition
- * @param throwError
- * whether or not to throw an exception if an error was found
- * @return a list if validation remarks
- */
- <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}(\"{1}\")", IQSchema.class.getSimpleName(), schema)));
- } else if (!schema.equalsIgnoreCase(def.schemaName)) {
- remarks.add(error(
- table,
- "SCHEMA",
- format("@{0}(\"{1}\") != {2}", IQSchema.class.getSimpleName(), def.schemaName, schema))
- .throwError(throwError));
- }
- }
-
- // index validation
- for (IndexInspector index : indexes.values()) {
- validate(remarks, def, index, throwError);
- }
-
- // field column validation
- for (FieldDefinition fieldDef : def.getFields()) {
- validate(remarks, fieldDef, throwError);
- }
- return remarks;
- }
-
- /**
- * Validates an inspected index from the database against the
- * IndexDefinition within the TableDefinition.
- */
- private <T> void validate(List<ValidationRemark> remarks, TableDefinition<T> def, IndexInspector index,
- boolean throwError) {
- List<IndexDefinition> defIndexes = def.getIndexes();
- if (defIndexes.size() > indexes.size()) {
- remarks.add(warn(table, IndexType.STANDARD.name(), "More model indexes than database indexes"));
- } else if (defIndexes.size() < indexes.size()) {
- remarks.add(warn(table, IndexType.STANDARD.name(), "Model class is missing indexes"));
- }
- // TODO complete index validation.
- // need to actually compare index types and columns within each index.
-
- // TODO add constraints validation
- List<ConstraintUniqueDefinition> defContraintsU = def.getContraintsUnique();
- List<ConstraintForeignKeyDefinition> defContraintsFK = def.getContraintsForeignKey();
- }
-
- /**
- * Validates a column against the model's field definition. Checks for
- * existence, supported type, type mapping, default value, defined lengths,
- * primary key, autoincrement.
- */
- private void validate(List<ValidationRemark> remarks, FieldDefinition fieldDef, boolean throwError) {
- // unknown field
- if (!columns.containsKey(fieldDef.columnName.toLowerCase())) {
- // unknown column mapping
- remarks.add(error(table, fieldDef, "Does not exist in database!").throwError(throwError));
- return;
- }
- ColumnInspector col = columns.get(fieldDef.columnName.toLowerCase());
- Class<?> fieldClass = fieldDef.field.getType();
- Class<?> jdbcClass = ModelUtils.getClassForSqlType(col.type, dateTimeClass);
-
- // supported type check
- // iciql maps to VARCHAR for unsupported types.
- if (fieldDef.dataType.equals("VARCHAR") && (fieldClass != String.class)) {
- remarks.add(error(table, fieldDef,
- "iciql does not currently implement support for " + fieldClass.getName()).throwError(
- throwError));
- }
- // number types
- if (!fieldClass.equals(jdbcClass)) {
- if (Number.class.isAssignableFrom(fieldClass)) {
- remarks.add(warn(
- table,
- col,
- format("Precision mismatch: ModelObject={0}, ColumnObject={1}",
- fieldClass.getSimpleName(), jdbcClass.getSimpleName())));
- } else {
- if (!Date.class.isAssignableFrom(jdbcClass)) {
- remarks.add(warn(
- table,
- col,
- format("Object Mismatch: ModelObject={0}, ColumnObject={1}",
- fieldClass.getSimpleName(), jdbcClass.getSimpleName())));
- }
- }
- }
-
- // string types
- if (fieldClass == String.class) {
- if ((fieldDef.length != col.size) && (col.size < Integer.MAX_VALUE)) {
- remarks.add(warn(
- table,
- col,
- format("{0}.length={1}, ColumnMaxLength={2}", IQColumn.class.getSimpleName(),
- fieldDef.length, col.size)));
- }
- if (fieldDef.length > 0 && !fieldDef.trim) {
- remarks.add(consider(table, col, format("{0}.trim=true will prevent IciqlExceptions on"
- + " INSERT or UPDATE, but will clip data!", IQColumn.class.getSimpleName())));
- }
- }
-
- // numeric autoIncrement
- if (fieldDef.isAutoIncrement != col.isAutoIncrement) {
- remarks.add(warn(
- table,
- col,
- format("{0}.autoIncrement={1}" + " while Column autoIncrement={2}",
- IQColumn.class.getSimpleName(), fieldDef.isAutoIncrement, col.isAutoIncrement)));
- }
- // default value
- if (!col.isAutoIncrement && !col.isPrimaryKey) {
- String defaultValue = null;
- if (fieldDef.defaultValue != null && fieldDef.defaultValue instanceof String) {
- defaultValue = fieldDef.defaultValue.toString();
- }
- // check Model.defaultValue format
- if (!ModelUtils.isProperlyFormattedDefaultValue(defaultValue)) {
- remarks.add(error(
- table,
- col,
- format("{0}.defaultValue=\"{1}\"" + " is improperly formatted!",
- IQColumn.class.getSimpleName(), defaultValue)).throwError(throwError));
- // next field
- return;
- }
- // compare Model.defaultValue to Column.defaultValue
- if (isNullOrEmpty(defaultValue) && !isNullOrEmpty(col.defaultValue)) {
- // Model.defaultValue is NULL, Column.defaultValue is NOT NULL
- remarks.add(warn(
- table,
- col,
- format("{0}.defaultValue=\"\"" + " while column default=\"{1}\"",
- IQColumn.class.getSimpleName(), col.defaultValue)));
- } else if (!isNullOrEmpty(defaultValue) && isNullOrEmpty(col.defaultValue)) {
- // Column.defaultValue is NULL, Model.defaultValue is NOT NULL
- remarks.add(warn(
- table,
- col,
- format("{0}.defaultValue=\"{1}\"" + " while column default=\"\"",
- IQColumn.class.getSimpleName(), defaultValue)));
- } else if (!isNullOrEmpty(defaultValue) && !isNullOrEmpty(col.defaultValue)) {
- if (!defaultValue.equals(col.defaultValue)) {
- // Model.defaultValue != Column.defaultValue
- remarks.add(warn(
- table,
- col,
- format("{0}.defaultValue=\"{1}\"" + " while column default=\"{2}\"",
- IQColumn.class.getSimpleName(), defaultValue, col.defaultValue)));
- }
- }
-
- // sanity check Model.defaultValue literal value
- if (!ModelUtils.isValidDefaultValue(fieldDef.field.getType(), defaultValue)) {
- remarks.add(error(
- table,
- col,
- format("{0}.defaultValue=\"{1}\" is invalid!", IQColumn.class.getSimpleName(),
- defaultValue)));
- }
- }
- }
-
- /**
- * Represents an index as it exists in the database.
- */
- private static class IndexInspector {
-
- String name;
- IndexType type;
- private List<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"));
- }
- }
-
- /**
- * Represents a column as it exists in the database.
- */
- static class ColumnInspector implements Comparable<ColumnInspector> {
- String name;
- String type;
- int size;
- boolean nullable;
- Class<?> clazz;
- boolean isPrimaryKey;
- boolean isAutoIncrement;
- String defaultValue;
-
- public int compareTo(ColumnInspector o) {
- if (isPrimaryKey && o.isPrimaryKey) {
- // both primary sort by name
- return name.compareTo(o.name);
- } else if (isPrimaryKey && !o.isPrimaryKey) {
- // primary first
- return -1;
- } else if (!isPrimaryKey && o.isPrimaryKey) {
- // primary first
- return 1;
- } else {
- // neither primary, sort by name
- return name.compareTo(o.name);
- }
- }
- }
-
- /**
- * Convenience class based on StatementBuilder for creating the annotation
- * parameter list.
- */
- private static class AnnotationBuilder extends StatementBuilder {
-
- AnnotationBuilder() {
- super();
- }
-
- void addParameter(String parameter) {
-
- appendExceptFirst(", ");
- append(parameter);
- }
-
- <T> void addParameter(String parameter, T value) {
- appendExceptFirst(", ");
- if (!StringUtils.isNullOrEmpty(parameter)) {
- append(parameter);
- append('=');
- }
- if (value instanceof List) {
- append("{ ");
- List<?> list = (List<?>) value;
- StatementBuilder flat = new StatementBuilder();
- for (Object o : list) {
- flat.appendExceptFirst(", ");
- if (o instanceof String) {
- flat.append('\"');
- }
- // TODO escape string
- flat.append(o.toString().trim());
- if (o instanceof String) {
- flat.append('\"');
- }
- }
- append(flat);
- append(" }");
- } else {
- if (value instanceof String) {
- append('\"');
- }
- // TODO escape
- append(value.toString().trim());
- if (value instanceof String) {
- append('\"');
- }
- }
- }
-
- void addEnum(String parameter, Enum<?> value) {
- appendExceptFirst(", ");
- if (!StringUtils.isNullOrEmpty(parameter)) {
- append(parameter);
- append('=');
- }
- append(value.getClass().getSimpleName() + "." + value.name());
- }
- }
+ }
+ if (primaryKeys.size() == 1) {
+ if (col.name.equalsIgnoreCase(primaryKeys.get(0))) {
+ col.isPrimaryKey = true;
+ }
+ }
+ if (!col.isAutoIncrement) {
+ col.defaultValue = rs.getString("COLUMN_DEF");
+ }
+ columns.put(col.name.toLowerCase(), col);
+ }
+ } finally {
+ closeSilently(rs);
+ }
+ }
+
+ /**
+ * Generates a model (class definition) from this table. The model includes
+ * indexes, primary keys, default values, lengths, and nullables.
+ * information.
+ * <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(Serializable.class.getCanonicalName());
+ imports.add(IQSchema.class.getCanonicalName());
+ imports.add(IQTable.class.getCanonicalName());
+ imports.add(IQIndexes.class.getCanonicalName());
+ imports.add(IQIndex.class.getCanonicalName());
+ imports.add(IQColumn.class.getCanonicalName());
+ imports.add(IndexType.class.getCanonicalName());
+
+ // fields
+ StringBuilder fields = new StringBuilder();
+ List<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(null, schema);
+ model.append(ap);
+ model.append(')').append(eol);
+ }
+
+ // @IQTable
+ model.append('@').append(IQTable.class.getSimpleName());
+ model.append('(');
+
+ // IQTable annotation parameters
+ AnnotationBuilder ap = new AnnotationBuilder();
+ ap.addParameter("name", table);
+
+ if (primaryKeys.size() > 1) {
+ ap.addParameter("primaryKey", primaryKeys);
+ }
+
+ // finish @IQTable annotation
+ model.append(ap);
+ model.append(')').append(eol);
+
+ // @IQIndexes
+ // @IQIndex
+ String indexAnnotations = generateIndexAnnotations();
+ if (!StringUtils.isNullOrEmpty(indexAnnotations)) {
+ model.append(indexAnnotations);
+ }
+
+ // class declaration
+ String clazzName = ModelUtils.convertTableToClassName(table);
+ model.append(format("public class {0} implements Serializable '{'", clazzName)).append(eol);
+ model.append(eol);
+ model.append("\tprivate static final long serialVersionUID = 1L;").append(eol);
+ model.append(eol);
+
+ // field declarations
+ model.append(fields);
+
+ // default constructor
+ model.append("\t" + "public ").append(clazzName).append("() {").append(eol);
+ model.append("\t}").append(eol);
+
+ // end of class body
+ model.append('}');
+ model.trimToSize();
+ return model.toString();
+ }
+
+ /**
+ * Generates the specified index annotation.
+ *
+ * @param ap
+ */
+ String generateIndexAnnotations() {
+ if (indexes == null || indexes.size() == 0) {
+ // no matching indexes
+ return null;
+ }
+ AnnotationBuilder ap = new AnnotationBuilder();
+ if (indexes.size() == 1) {
+ // single index
+ IndexInspector index = indexes.values().toArray(new IndexInspector[1])[0];
+ ap.append(generateIndexAnnotation(index));
+ ap.append(eol);
+ } else {
+ // multiple indexes
+ ap.append('@').append(IQIndexes.class.getSimpleName());
+ ap.append("({");
+ ap.resetCount();
+ for (IndexInspector index : indexes.values()) {
+ ap.appendExceptFirst(", ");
+ ap.append(generateIndexAnnotation(index));
+ }
+ ap.append("})").append(eol);
+ }
+ return ap.toString();
+ }
+
+ private String generateIndexAnnotation(IndexInspector index) {
+ AnnotationBuilder ap = new AnnotationBuilder();
+ ap.append('@').append(IQIndex.class.getSimpleName());
+ ap.append('(');
+ ap.resetCount();
+ if (!StringUtils.isNullOrEmpty(index.name)) {
+ ap.addParameter("name", index.name);
+ }
+ if (!index.type.equals(IndexType.STANDARD)) {
+ ap.addEnum("type", index.type);
+ }
+ if (ap.getCount() > 0) {
+ // multiple fields specified
+ ap.addParameter("value", index.columns);
+ } else {
+ // default value
+ ap.addParameter(null, index.columns);
+ }
+ ap.append(')');
+ return ap.toString();
+ }
+
+ private StatementBuilder generateColumn(Set<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 {
+ // Imports
+ // don't import primitives, java.lang classes, or byte []
+ if (clazz.getPackage() == null) {
+ } else if (clazz.getPackage().getName().equals("java.lang")) {
+ } else if (clazz.equals(byte[].class)) {
+ } else {
+ imports.add(clazz.getCanonicalName());
+ }
+ // @IQColumn
+ sb.append('@').append(IQColumn.class.getSimpleName());
+
+ // IQColumn annotation parameters
+ AnnotationBuilder ap = new AnnotationBuilder();
+
+ // IQColumn.name
+ if (!col.name.equalsIgnoreCase(column)) {
+ ap.addParameter("name", col.name);
+ }
+
+ // IQColumn.primaryKey
+ // composite primary keys are annotated on the table
+ if (col.isPrimaryKey && primaryKeys.size() == 1) {
+ ap.addParameter("primaryKey=true");
+ }
+
+ // IQColumn.length
+ if ((clazz == String.class) && (col.size > 0) && (col.size < Integer.MAX_VALUE)) {
+ ap.addParameter("length", col.size);
+
+ // IQColumn.trim
+ if (trimStrings) {
+ ap.addParameter("trim=true");
+ }
+ } else {
+ // IQColumn.AutoIncrement
+ if (col.isAutoIncrement) {
+ ap.addParameter("autoIncrement=true");
+ }
+ }
+
+ // IQColumn.nullable
+ if (!col.nullable) {
+ ap.addParameter("nullable=false");
+ }
+
+ // IQColumn.defaultValue
+ if (!isNullOrEmpty(col.defaultValue)) {
+ ap.addParameter("defaultValue=\"" + col.defaultValue + "\"");
+ }
+
+ // add leading and trailing ()
+ if (ap.length() > 0) {
+ ap.insert(0, '(');
+ ap.append(')');
+ }
+ sb.append(ap);
+ }
+ sb.append(eol);
+
+ // variable declaration
+ sb.append("\t" + "public ");
+ sb.append(clazz.getSimpleName());
+ sb.append(' ');
+ sb.append(column);
+ sb.append(';');
+ sb.append(eol).append(eol);
+ return sb;
+ }
+
+ /**
+ * Validates that a table definition (annotated, interface, or both) matches
+ * the current state of the table and indexes in the database. Results are
+ * returned as a list of validation remarks which includes recommendations,
+ * warnings, and errors about the model. The caller may choose to have
+ * validate throw an exception on any validation ERROR.
+ *
+ * @param def the table definition
+ * @param throwError whether or not to throw an exception if an error was found
+ * @return a list if validation remarks
+ */
+ <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}(\"{1}\")", IQSchema.class.getSimpleName(), schema)));
+ } else if (!schema.equalsIgnoreCase(def.schemaName)) {
+ remarks.add(error(
+ table,
+ "SCHEMA",
+ format("@{0}(\"{1}\") != {2}", IQSchema.class.getSimpleName(), def.schemaName, schema))
+ .throwError(throwError));
+ }
+ }
+
+ // index validation
+ for (IndexInspector index : indexes.values()) {
+ validate(remarks, def, index, throwError);
+ }
+
+ // field column validation
+ for (FieldDefinition fieldDef : def.getFields()) {
+ validate(remarks, fieldDef, throwError);
+ }
+ return remarks;
+ }
+
+ /**
+ * Validates an inspected index from the database against the
+ * IndexDefinition within the TableDefinition.
+ */
+ private <T> void validate(List<ValidationRemark> remarks, TableDefinition<T> def, IndexInspector index,
+ boolean throwError) {
+ List<IndexDefinition> defIndexes = def.getIndexes();
+ if (defIndexes.size() > indexes.size()) {
+ remarks.add(warn(table, IndexType.STANDARD.name(), "More model indexes than database indexes"));
+ } else if (defIndexes.size() < indexes.size()) {
+ remarks.add(warn(table, IndexType.STANDARD.name(), "Model class is missing indexes"));
+ }
+ // TODO complete index validation.
+ // need to actually compare index types and columns within each index.
+
+ // TODO add constraints validation
+ List<ConstraintUniqueDefinition> defContraintsU = def.getContraintsUnique();
+ List<ConstraintForeignKeyDefinition> defContraintsFK = def.getContraintsForeignKey();
+ }
+
+ /**
+ * Validates a column against the model's field definition. Checks for
+ * existence, supported type, type mapping, default value, defined lengths,
+ * primary key, autoincrement.
+ */
+ private void validate(List<ValidationRemark> remarks, FieldDefinition fieldDef, boolean throwError) {
+ // unknown field
+ if (!columns.containsKey(fieldDef.columnName.toLowerCase())) {
+ // unknown column mapping
+ remarks.add(error(table, fieldDef, "Does not exist in database!").throwError(throwError));
+ return;
+ }
+ ColumnInspector col = columns.get(fieldDef.columnName.toLowerCase());
+ Class<?> fieldClass = fieldDef.field.getType();
+ Class<?> jdbcClass = ModelUtils.getClassForSqlType(col.type, dateTimeClass);
+
+ // supported type check
+ // iciql maps to VARCHAR for unsupported types.
+ if (fieldDef.dataType.equals("VARCHAR") && (fieldClass != String.class)) {
+ remarks.add(error(table, fieldDef,
+ "iciql does not currently implement support for " + fieldClass.getName()).throwError(
+ throwError));
+ }
+ // number types
+ if (!fieldClass.equals(jdbcClass)) {
+ if (Number.class.isAssignableFrom(fieldClass)) {
+ remarks.add(warn(
+ table,
+ col,
+ format("Precision mismatch: ModelObject={0}, ColumnObject={1}",
+ fieldClass.getSimpleName(), jdbcClass.getSimpleName())));
+ } else {
+ if (!Date.class.isAssignableFrom(jdbcClass)) {
+ remarks.add(warn(
+ table,
+ col,
+ format("Object Mismatch: ModelObject={0}, ColumnObject={1}",
+ fieldClass.getSimpleName(), jdbcClass.getSimpleName())));
+ }
+ }
+ }
+
+ // string types
+ if (fieldClass == String.class) {
+ if ((fieldDef.length != col.size) && (col.size < Integer.MAX_VALUE)) {
+ remarks.add(warn(
+ table,
+ col,
+ format("{0}.length={1}, ColumnMaxLength={2}", IQColumn.class.getSimpleName(),
+ fieldDef.length, col.size)));
+ }
+ if (fieldDef.length > 0 && !fieldDef.trim) {
+ remarks.add(consider(table, col, format("{0}.trim=true will prevent IciqlExceptions on"
+ + " INSERT or UPDATE, but will clip data!", IQColumn.class.getSimpleName())));
+ }
+ }
+
+ // numeric autoIncrement
+ if (fieldDef.isAutoIncrement != col.isAutoIncrement) {
+ remarks.add(warn(
+ table,
+ col,
+ format("{0}.autoIncrement={1}" + " while Column autoIncrement={2}",
+ IQColumn.class.getSimpleName(), fieldDef.isAutoIncrement, col.isAutoIncrement)));
+ }
+ // default value
+ if (!col.isAutoIncrement && !col.isPrimaryKey) {
+ String defaultValue = null;
+ if (fieldDef.defaultValue != null && fieldDef.defaultValue instanceof String) {
+ defaultValue = fieldDef.defaultValue.toString();
+ }
+ // check Model.defaultValue format
+ if (!ModelUtils.isProperlyFormattedDefaultValue(defaultValue)) {
+ remarks.add(error(
+ table,
+ col,
+ format("{0}.defaultValue=\"{1}\"" + " is improperly formatted!",
+ IQColumn.class.getSimpleName(), defaultValue)).throwError(throwError));
+ // next field
+ return;
+ }
+ // compare Model.defaultValue to Column.defaultValue
+ if (isNullOrEmpty(defaultValue) && !isNullOrEmpty(col.defaultValue)) {
+ // Model.defaultValue is NULL, Column.defaultValue is NOT NULL
+ remarks.add(warn(
+ table,
+ col,
+ format("{0}.defaultValue=\"\"" + " while column default=\"{1}\"",
+ IQColumn.class.getSimpleName(), col.defaultValue)));
+ } else if (!isNullOrEmpty(defaultValue) && isNullOrEmpty(col.defaultValue)) {
+ // Column.defaultValue is NULL, Model.defaultValue is NOT NULL
+ remarks.add(warn(
+ table,
+ col,
+ format("{0}.defaultValue=\"{1}\"" + " while column default=\"\"",
+ IQColumn.class.getSimpleName(), defaultValue)));
+ } else if (!isNullOrEmpty(defaultValue) && !isNullOrEmpty(col.defaultValue)) {
+ if (!defaultValue.equals(col.defaultValue)) {
+ // Model.defaultValue != Column.defaultValue
+ remarks.add(warn(
+ table,
+ col,
+ format("{0}.defaultValue=\"{1}\"" + " while column default=\"{2}\"",
+ IQColumn.class.getSimpleName(), defaultValue, col.defaultValue)));
+ }
+ }
+
+ // sanity check Model.defaultValue literal value
+ if (!ModelUtils.isValidDefaultValue(fieldDef.field.getType(), defaultValue)) {
+ remarks.add(error(
+ table,
+ col,
+ format("{0}.defaultValue=\"{1}\" is invalid!", IQColumn.class.getSimpleName(),
+ defaultValue)));
+ }
+ }
+ }
+
+ /**
+ * Represents an index as it exists in the database.
+ */
+ private static class IndexInspector {
+
+ String name;
+ IndexType type;
+ private List<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"));
+ }
+ }
+
+ /**
+ * Represents a column as it exists in the database.
+ */
+ static class ColumnInspector implements Comparable<ColumnInspector> {
+ String name;
+ String type;
+ int size;
+ boolean nullable;
+ Class<?> clazz;
+ boolean isPrimaryKey;
+ boolean isAutoIncrement;
+ String defaultValue;
+
+ public int compareTo(ColumnInspector o) {
+ if (isPrimaryKey && o.isPrimaryKey) {
+ // both primary sort by name
+ return name.compareTo(o.name);
+ } else if (isPrimaryKey && !o.isPrimaryKey) {
+ // primary first
+ return -1;
+ } else if (!isPrimaryKey && o.isPrimaryKey) {
+ // primary first
+ return 1;
+ } else {
+ // neither primary, sort by name
+ return name.compareTo(o.name);
+ }
+ }
+ }
+
+ /**
+ * Convenience class based on StatementBuilder for creating the annotation
+ * parameter list.
+ */
+ private static class AnnotationBuilder extends StatementBuilder {
+
+ AnnotationBuilder() {
+ super();
+ }
+
+ void addParameter(String parameter) {
+
+ appendExceptFirst(", ");
+ append(parameter);
+ }
+
+ <T> void addParameter(String parameter, T value) {
+ appendExceptFirst(", ");
+ if (!StringUtils.isNullOrEmpty(parameter)) {
+ append(parameter);
+ append('=');
+ }
+ if (value instanceof List) {
+ append("{ ");
+ List<?> list = (List<?>) value;
+ StatementBuilder flat = new StatementBuilder();
+ for (Object o : list) {
+ flat.appendExceptFirst(", ");
+ if (o instanceof String) {
+ flat.append('\"');
+ }
+ // TODO escape string
+ flat.append(o.toString().trim());
+ if (o instanceof String) {
+ flat.append('\"');
+ }
+ }
+ append(flat);
+ append(" }");
+ } else {
+ if (value instanceof String) {
+ append('\"');
+ }
+ // TODO escape
+ append(value.toString().trim());
+ if (value instanceof String) {
+ append('\"');
+ }
+ }
+ }
+
+ void addEnum(String parameter, Enum<?> value) {
+ appendExceptFirst(", ");
+ if (!StringUtils.isNullOrEmpty(parameter)) {
+ append(parameter);
+ append('=');
+ }
+ append(value.getClass().getSimpleName() + "." + value.name());
+ }
+ }
} \ No newline at end of file