From b055a2a49335c78fdc754e38a7e8ab863b2a5515 Mon Sep 17 00:00:00 2001 From: James Moger Date: Thu, 4 Aug 2011 17:58:22 -0400 Subject: BLOB support (issue 1) and Enum support (issue 2). Documentation. --- build.xml | 4 + docs/01_model_classes.mkd | 17 +++- docs/05_releases.mkd | 2 + src/com/iciql/Iciql.java | 52 +++++++++- src/com/iciql/ModelUtils.java | 38 +++++-- src/com/iciql/Query.java | 3 + src/com/iciql/TableDefinition.java | 38 ++++++- src/com/iciql/TableInspector.java | 6 +- src/com/iciql/build/BuildSite.java | 7 ++ src/com/iciql/util/Utils.java | 111 +++++++++++++++++++++ tests/com/iciql/test/ModelsTest.java | 2 +- .../iciql/test/models/ProductAnnotationOnly.java | 3 +- tests/com/iciql/test/models/SupportedTypes.java | 64 ++++++++++++ 13 files changed, 331 insertions(+), 16 deletions(-) diff --git a/build.xml b/build.xml index 8c7eaec..18698c6 100644 --- a/build.xml +++ b/build.xml @@ -282,6 +282,10 @@ + + + + diff --git a/docs/01_model_classes.mkd b/docs/01_model_classes.mkd index a0db6a1..8afcfa8 100644 --- a/docs/01_model_classes.mkd +++ b/docs/01_model_classes.mkd @@ -47,6 +47,12 @@ Alternatively, model classes can be automatically generated by iciql using the m java.util.Date TIMESTAMP +byte [] +BLOB + +java.lang.Enum +VARCHAR/TEXT *@IQEnum(STRING)* or INT *@IQEnum(ORDINAL)* + **NOTE:**
@@ -55,7 +61,6 @@ Please consult the `com.iciql.ModelUtils` class for details. ### Unsupported Types - Java primitives (use their object counterparts instead) -- binary types (BLOB, etc) - array types - custom types @@ -83,7 +88,9 @@ The recommended approach to setup a model class is to annotate the class and fie ### Example Annotated Model %BEGINCODE% +import com.iciql.Iciql.EnumType; import com.iciql.Iciql.IQColumn; +import com.iciql.Iciql.IQEnum; import com.iciql.Iciql.IQIndex; import com.iciql.Iciql.IQTable; @@ -94,6 +101,11 @@ import com.iciql.Iciql.IQTable; }) public class Product { + @IQEnum(EnumType.ORDINAL) + public enum Availability { + ACTIVE, DISCONTINUED; + } + @IQColumn(primaryKey = true) public Integer productId; @@ -111,6 +123,9 @@ public class Product { @IQColumn private Integer reorderQuantity; + + @IQColumn + private Availability availability; public Product() { // default constructor diff --git a/docs/05_releases.mkd b/docs/05_releases.mkd index db29860..50fda43 100644 --- a/docs/05_releases.mkd +++ b/docs/05_releases.mkd @@ -3,6 +3,8 @@ ### Current Release **%VERSION%** ([zip](http://code.google.com/p/iciql/downloads/detail?name=%ZIP%)|[jar](http://code.google.com/p/iciql/downloads/detail?name=%JAR%))   *released %BUILDDATE%* +- added BLOB support (issue 1) +- added java.lang.Enum support (issue 2) - api change release (API v2) - annotations overhaul to reduce verbosity - @IQSchema(name="public") -> @IQSchema("public") diff --git a/src/com/iciql/Iciql.java b/src/com/iciql/Iciql.java index 35ad8cc..8d71aa6 100644 --- a/src/com/iciql/Iciql.java +++ b/src/com/iciql/Iciql.java @@ -87,9 +87,13 @@ import java.lang.annotation.Target; * java.util.Date * TIMESTAMP * + * + * byte [] + * BLOB + * * *

- * Unsupported data types: binary types (BLOB, etc), and custom types. + * Unsupported data types: java.lang.Enum, Array Types, and custom types. *

* 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 @@ -142,6 +146,7 @@ public interface Iciql { /** * An annotation for an iciql version. *

+ * * @IQVersion(1) */ @Retention(RetentionPolicy.RUNTIME) @@ -162,6 +167,7 @@ public interface Iciql { /** * An annotation for a schema. *

+ * * @IQSchema("PUBLIC") */ @Retention(RetentionPolicy.RUNTIME) @@ -189,7 +195,8 @@ public interface Iciql { *

  • @IQIndex("name") *
  • @IQIndex({"street", "city"}) *
  • @IQIndex(name="streetidx", value={"street", "city"}) - *
  • @IQIndex(name="addressidx", type=IndexType.UNIQUE, value={"house_number", "street", "city"}) + *
  • @IQIndex(name="addressidx", type=IndexType.UNIQUE, + * value={"house_number", "street", "city"}) * */ @Retention(RetentionPolicy.RUNTIME) @@ -365,6 +372,47 @@ public interface Iciql { } + /** + * Enumeration representing now to map a java.lang.Enum to a column. + *

    + *

      + *
    • STRING + *
    • ORDINAL + *
    + */ + public enum EnumType { + STRING, ORDINAL; + } + + /** + * Annotation to define how a java.lang.Enum is mapped to a column. + *

    + * This annotation can be used on: + *

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

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

    +	 * IQEnum(EnumType.STRING)
    +	 * 
    + * + * A string mapping will generate either a VARCHAR, if IQColumn.maxLength > + * 0 or a TEXT column if IQColumn.maxLength == 0 + * + */ + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.FIELD, ElementType.TYPE }) + public @interface IQEnum { + EnumType value() default EnumType.STRING; + } + /** * This method is called to let the table define the primary key, indexes, * and the table name. diff --git a/src/com/iciql/ModelUtils.java b/src/com/iciql/ModelUtils.java index 6b28f0e..45a4882 100644 --- a/src/com/iciql/ModelUtils.java +++ b/src/com/iciql/ModelUtils.java @@ -57,7 +57,7 @@ class ModelUtils { 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? + m.put(byte[].class, "BLOB"); } /** @@ -111,14 +111,21 @@ class ModelUtils { // date m.put("DATETIME", "TIMESTAMP"); m.put("SMALLDATETIME", "TIMESTAMP"); + + // binary types + m.put("TINYBLOB", "BLOB"); + m.put("MEDIUMBLOB", "BLOB"); + m.put("LONGBLOB", "BLOB"); + m.put("IMAGE", "BLOB"); + m.put("OID", "BLOB"); } - private static final List KEYWORDS = Arrays.asList("abstract", "assert", "boolean", "break", "byte", - "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "enum", - "extends", "final", "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", - "interface", "long", "native", "new", "package", "private", "protected", "public", "return", "short", - "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", - "void", "volatile", "while", "false", "null", "true"); + private static final List KEYWORDS = Arrays.asList("abstract", "assert", "boolean", "break", + "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", + "enum", "extends", "final", "finally", "float", "for", "goto", "if", "implements", "import", + "instanceof", "int", "interface", "long", "native", "new", "package", "private", "protected", + "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", + "throw", "throws", "transient", "try", "void", "volatile", "while", "false", "null", "true"); /** * Returns a SQL type mapping for a Java class. @@ -131,6 +138,20 @@ class ModelUtils { */ static String getDataType(FieldDefinition fieldDef, boolean strictTypeMapping) { Class fieldClass = fieldDef.field.getType(); + if (fieldClass.isEnum()) { + if (fieldDef.enumType == null) { + throw new IciqlException(fieldDef.field.getName() + " enum field does not specify @IQEnum!"); + } + switch (fieldDef.enumType) { + case STRING: + if (fieldDef.maxLength <= 0) { + return "TEXT"; + } + return "VARCHAR"; + case ORDINAL: + return "INT"; + } + } if (SUPPORTED_TYPES.containsKey(fieldClass)) { String type = SUPPORTED_TYPES.get(fieldClass); if (type.equals("VARCHAR") && fieldDef.maxLength <= 0) { @@ -228,7 +249,8 @@ class ModelUtils { } Pattern literalDefault = Pattern.compile("'.*'"); Pattern functionDefault = Pattern.compile("[^'].*[^']"); - return literalDefault.matcher(defaultValue).matches() || functionDefault.matcher(defaultValue).matches(); + return literalDefault.matcher(defaultValue).matches() + || functionDefault.matcher(defaultValue).matches(); } /** diff --git a/src/com/iciql/Query.java b/src/com/iciql/Query.java index 97e143b..d9dc84f 100644 --- a/src/com/iciql/Query.java +++ b/src/com/iciql/Query.java @@ -18,6 +18,7 @@ package com.iciql; import java.lang.reflect.Field; +import java.sql.Blob; import java.sql.Clob; import java.sql.ResultSet; import java.sql.SQLException; @@ -223,6 +224,8 @@ public class Query { Object o = rs.getObject(1); if (Clob.class.isAssignableFrom(o.getClass())) { value = (X) Utils.convert(o, String.class); + } else if (Blob.class.isAssignableFrom(o.getClass())) { + value = (X) Utils.convert(o, byte[].class); } else { value = (X) o; } diff --git a/src/com/iciql/TableDefinition.java b/src/com/iciql/TableDefinition.java index 456d0f4..a38ac51 100644 --- a/src/com/iciql/TableDefinition.java +++ b/src/com/iciql/TableDefinition.java @@ -27,7 +27,9 @@ import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import com.iciql.Iciql.EnumType; import com.iciql.Iciql.IQColumn; +import com.iciql.Iciql.IQEnum; import com.iciql.Iciql.IQIndex; import com.iciql.Iciql.IQIndexes; import com.iciql.Iciql.IQSchema; @@ -74,6 +76,7 @@ class TableDefinition { boolean trimString; boolean allowNull; String defaultValue; + EnumType enumType; Object getValue(Object obj) { try { @@ -264,6 +267,7 @@ class TableDefinition { int maxLength = 0; boolean trimString = false; boolean allowNull = true; + EnumType enumType = null; String defaultValue = ""; boolean hasAnnotation = f.isAnnotationPresent(IQColumn.class); if (hasAnnotation) { @@ -278,6 +282,21 @@ class TableDefinition { allowNull = col.allowNull(); defaultValue = col.defaultValue(); } + + // configure Java -> SQL enum mapping + if (f.getType().isEnum()) { + if (f.getType().isAnnotationPresent(IQEnum.class)) { + // enum definition is annotated for all instances + IQEnum iqenum = f.getType().getAnnotation(IQEnum.class); + enumType = iqenum.value(); + } + if (f.isAnnotationPresent(IQEnum.class)) { + // this instance of the enum is annotated + IQEnum iqenum = f.getAnnotation(IQEnum.class); + enumType = iqenum.value(); + } + } + boolean isPublic = Modifier.isPublic(f.getModifiers()); boolean reflectiveMatch = isPublic && !byAnnotationsOnly; if (reflectiveMatch || hasAnnotation) { @@ -290,6 +309,7 @@ class TableDefinition { fieldDef.trimString = trimString; fieldDef.allowNull = allowNull; fieldDef.defaultValue = defaultValue; + fieldDef.enumType = enumType; fieldDef.dataType = ModelUtils.getDataType(fieldDef, strictTypeMapping); fields.add(fieldDef); } @@ -306,10 +326,26 @@ class TableDefinition { } /** - * Optionally truncates strings to the maximum length + * Optionally truncates strings to the maximum length and converts + * java.lang.Enum types to Strings or Integers. */ private Object getValue(Object obj, FieldDefinition field) { Object value = field.getValue(obj); + if (field.enumType != null) { + // convert enumeration to INT or STRING + Enum iqenum = (Enum) value; + switch (field.enumType) { + case STRING: + if (field.trimString && field.maxLength > 0) { + if (iqenum.name().length() > field.maxLength) { + return iqenum.name().substring(0, field.maxLength); + } + } + return iqenum.name(); + case ORDINAL: + return iqenum.ordinal(); + } + } if (field.trimString && field.maxLength > 0) { if (value instanceof String) { // clip strings diff --git a/src/com/iciql/TableInspector.java b/src/com/iciql/TableInspector.java index 8e537b4..879e23a 100644 --- a/src/com/iciql/TableInspector.java +++ b/src/com/iciql/TableInspector.java @@ -324,8 +324,12 @@ public class TableInspector { clazz = Object.class; sb.append("// unsupported type " + col.type); } else { + // Imports + // don't import byte [] + if (!clazz.equals(byte[].class)) { + imports.add(clazz.getCanonicalName()); + } // @IQColumn - imports.add(clazz.getCanonicalName()); sb.append('@').append(IQColumn.class.getSimpleName()); // IQColumn annotation parameters diff --git a/src/com/iciql/build/BuildSite.java b/src/com/iciql/build/BuildSite.java index 0686def..4ff5e68 100644 --- a/src/com/iciql/build/BuildSite.java +++ b/src/com/iciql/build/BuildSite.java @@ -189,6 +189,10 @@ public class BuildSite { String[] kv = token.split("=", 2); content = content.replace(kv[0], kv[1]); } + for (String token : params.regex) { + String[] kv = token.split("!!!", 2); + content = content.replaceAll(kv[0], kv[1]); + } OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(new File(destinationFolder, fileName)), Charset.forName("UTF-8")); @@ -315,6 +319,9 @@ public class BuildSite { @Parameter(names = { "--nomarkdown" }, description = "%STARTTOKEN%:%ENDTOKEN%", required = false) public List nomarkdown = new ArrayList(); + + @Parameter(names = { "--regex" }, description = "searchPattern!!!replacePattern", required = false) + public List regex = new ArrayList(); } } diff --git a/src/com/iciql/util/Utils.java b/src/com/iciql/util/Utils.java index 3a600fa..39875fb 100644 --- a/src/com/iciql/util/Utils.java +++ b/src/com/iciql/util/Utils.java @@ -17,12 +17,15 @@ package com.iciql.util; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.Reader; import java.io.StringWriter; import java.lang.reflect.Constructor; import java.math.BigDecimal; import java.math.BigInteger; +import java.sql.Blob; import java.sql.Clob; import java.util.ArrayList; import java.util.Collection; @@ -148,6 +151,12 @@ public class Utils { return (T) new java.sql.Timestamp(COUNTER.getAndIncrement()); } else if (clazz == java.util.Date.class) { return (T) new java.util.Date(COUNTER.getAndIncrement()); + } else if (clazz == byte[].class) { + return (T) new byte[0]; + } else if (clazz.isEnum()) { + // enums can not be instantiated reflectively + // return first constant as reference + return clazz.getEnumConstants()[0]; } else if (clazz == List.class) { return (T) new ArrayList(); } @@ -200,6 +209,11 @@ public class Utils { if (targetType.isAssignableFrom(currentType)) { return o; } + // convert enum + if (targetType.isEnum()) { + return convertEnum(o, targetType); + } + // convert from CLOB/TEXT/VARCHAR to String if (targetType == String.class) { if (Clob.class.isAssignableFrom(currentType)) { Clob c = (Clob) o; @@ -212,6 +226,8 @@ public class Utils { } return o.toString(); } + + // convert from number to number if (Number.class.isAssignableFrom(currentType)) { Number n = (Number) o; if (targetType == Byte.class) { @@ -228,6 +244,67 @@ public class Utils { return n.floatValue(); } } + + // convert from BLOB + if (targetType == byte[].class) { + if (Blob.class.isAssignableFrom(currentType)) { + Blob b = (Blob) o; + try { + InputStream is = b.getBinaryStream(); + return readBlobAndClose(is, -1); + } catch (Exception e) { + throw new IciqlException("Error converting BLOB to byte[]: " + e.toString(), e); + } + } + } + + throw new IciqlException("Can not convert the value " + o + " from " + currentType + " to " + + targetType); + } + + private static Object convertEnum(Object o, Class targetType) { + if (o == null) { + return null; + } + Class currentType = o.getClass(); + // convert from VARCHAR/TEXT/INT to Enum + Enum[] values = (Enum[]) targetType.getEnumConstants(); + if (Clob.class.isAssignableFrom(currentType)) { + // TEXT/CLOB field + Clob c = (Clob) o; + String name = null; + try { + Reader r = c.getCharacterStream(); + name = readStringAndClose(r, -1); + } catch (Exception e) { + throw new IciqlException("Error converting CLOB to String: " + e.toString(), e); + } + + // find name match + for (Enum value : values) { + if (value.name().equalsIgnoreCase(name)) { + return value; + } + } + } else if (String.class.isAssignableFrom(currentType)) { + // VARCHAR field + String name = (String) o; + for (Enum value : values) { + if (value.name().equalsIgnoreCase(name)) { + return value; + } + } + } else if (Number.class.isAssignableFrom(currentType)) { + // INT field + int n = ((Number) o).intValue(); + + // ORDINAL mapping + for (Enum value : values) { + if (value.ordinal() == n) { + return value; + } + } + } throw new IciqlException("Can not convert the value " + o + " from " + currentType + " to " + targetType); } @@ -264,4 +341,38 @@ public class Utils { in.close(); } } + + /** + * Read a number of bytes from a stream and close it. + * + * @param in + * the stream + * @param length + * the maximum number of bytes to read, or -1 to read until the + * end of file + * @return the string read + */ + public static byte[] readBlobAndClose(InputStream in, int length) throws IOException { + try { + if (length <= 0) { + length = Integer.MAX_VALUE; + } + int block = Math.min(BUFFER_BLOCK_SIZE, length); + ByteArrayOutputStream out = new ByteArrayOutputStream(length == Integer.MAX_VALUE ? block + : length); + byte[] buff = new byte[block]; + while (length > 0) { + int len = Math.min(block, length); + len = in.read(buff, 0, len); + if (len < 0) { + break; + } + out.write(buff, 0, len); + length -= len; + } + return out.toByteArray(); + } finally { + in.close(); + } + } } diff --git a/tests/com/iciql/test/ModelsTest.java b/tests/com/iciql/test/ModelsTest.java index 851da92..bafc3e0 100644 --- a/tests/com/iciql/test/ModelsTest.java +++ b/tests/com/iciql/test/ModelsTest.java @@ -115,7 +115,7 @@ public class ModelsTest { true); assertEquals(1, models.size()); // a poor test, but a start - assertEquals(1564, models.get(0).length()); + assertEquals(1838, models.get(0).length()); } @Test diff --git a/tests/com/iciql/test/models/ProductAnnotationOnly.java b/tests/com/iciql/test/models/ProductAnnotationOnly.java index 6b8d420..caf07c7 100644 --- a/tests/com/iciql/test/models/ProductAnnotationOnly.java +++ b/tests/com/iciql/test/models/ProductAnnotationOnly.java @@ -31,8 +31,7 @@ import com.iciql.Iciql.IndexType; */ @IQTable(name = "AnnotatedProduct", primaryKey = "id") -@IQIndexes({ @IQIndex({ "name", "cat" }), - @IQIndex(name = "nameidx", type = IndexType.HASH, value = "name") }) +@IQIndexes({ @IQIndex({ "name", "cat" }), @IQIndex(name = "nameidx", type = IndexType.HASH, value = "name") }) public class ProductAnnotationOnly { @IQColumn(autoIncrement = true) diff --git a/tests/com/iciql/test/models/SupportedTypes.java b/tests/com/iciql/test/models/SupportedTypes.java index 66c25d4..bb5ecc6 100644 --- a/tests/com/iciql/test/models/SupportedTypes.java +++ b/tests/com/iciql/test/models/SupportedTypes.java @@ -21,7 +21,9 @@ import java.math.BigDecimal; import java.util.List; import java.util.Random; +import com.iciql.Iciql.EnumType; import com.iciql.Iciql.IQColumn; +import com.iciql.Iciql.IQEnum; import com.iciql.Iciql.IQIndex; import com.iciql.Iciql.IQIndexes; import com.iciql.Iciql.IQTable; @@ -39,6 +41,29 @@ public class SupportedTypes { public static final SupportedTypes SAMPLE = new SupportedTypes(); + /** + * Test of plain enumeration. + * + * Each field declaraton of this enum must specify a mapping strategy. + */ + public enum Flower { + ROSE, TULIP, MUM, PETUNIA, MARIGOLD, DAFFODIL; + } + + /** + * Test of @IQEnum annotated enumeration. + * This strategy is the default strategy for all fields of the Tree enum. + * + * Individual Tree field declarations can override this strategy by + * specifying a different @IQEnum annotation. + * + * Here ORDINAL specifies that this enum will be mapped to an INT column. + */ + @IQEnum(EnumType.ORDINAL) + public enum Tree { + PINE, OAK, BIRCH, WALNUT, MAPLE; + } + @IQColumn(primaryKey = true, autoIncrement = true) public Integer id; @@ -81,6 +106,22 @@ public class SupportedTypes { @IQColumn private java.sql.Timestamp mySqlTimestamp; + @IQColumn + private byte[] myBlob; + + @IQEnum(EnumType.STRING) + @IQColumn(trimString = true, maxLength = 25) + private Flower myFavoriteFlower; + + @IQEnum(EnumType.ORDINAL) + @IQColumn + private Flower myOtherFavoriteFlower; + + @IQColumn(maxLength = 25) + // @IQEnum is set on the enumeration definition and is shared + // by all uses of Tree as an @IQColumn + private Tree myFavoriteTree; + public static List createList() { List list = Utils.newArrayList(); for (int i = 0; i < 10; i++) { @@ -105,6 +146,10 @@ public class SupportedTypes { s.mySqlDate = new java.sql.Date(rand.nextLong()); s.mySqlTime = new java.sql.Time(rand.nextLong()); s.mySqlTimestamp = new java.sql.Timestamp(rand.nextLong()); + s.myBlob = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + s.myFavoriteFlower = Flower.MUM; + s.myOtherFavoriteFlower = Flower.MARIGOLD; + s.myFavoriteTree = Tree.BIRCH; return s; } @@ -123,9 +168,28 @@ public class SupportedTypes { same &= mySqlDate.toString().equals(s.mySqlDate.toString()); same &= mySqlTime.toString().equals(s.mySqlTime.toString()); same &= myString.equals(s.myString); + same &= compare(myBlob, s.myBlob); + same &= myFavoriteFlower.equals(s.myFavoriteFlower); + same &= myOtherFavoriteFlower.equals(s.myOtherFavoriteFlower); + same &= myFavoriteTree.equals(s.myFavoriteTree); return same; } + private boolean compare(byte[] a, byte[] b) { + if (b == null) { + return false; + } + if (a.length != b.length) { + return false; + } + for (int i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; + } + /** * This class demonstrates the table upgrade. */ -- cgit v1.2.3