From 06c36fbed2732e5aeeeab3ff636e72aafbce815b Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Sat, 19 Jun 2010 17:33:55 +0000 Subject: [PATCH] - Added support for @Transform annotation. - Revamped column type determination to use a mechanism that supports delegation and does not rely on a heap of instanceof calls (which results in runtime failure rather than compile time failure when a new column type is added) - Discovered that HSQLDB (at least) does not handle Byte, Short and Float correctly. It returns them as Integer, Integer and Double respectively, which then causes failure when trying to write those values back to the fields. We work around this by converting non-null results to the correct type after we get them back from the JDBC driver. --- .../depot/impl/FieldMarshaller.java | 362 +++++++++++++++--- 1 file changed, 303 insertions(+), 59 deletions(-) diff --git a/src/java/com/samskivert/depot/impl/FieldMarshaller.java b/src/java/com/samskivert/depot/impl/FieldMarshaller.java index 35aab77..9012453 100644 --- a/src/java/com/samskivert/depot/impl/FieldMarshaller.java +++ b/src/java/com/samskivert/depot/impl/FieldMarshaller.java @@ -21,6 +21,7 @@ package com.samskivert.depot.impl; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.sql.Blob; @@ -33,13 +34,16 @@ import java.sql.Time; import java.sql.Timestamp; import com.samskivert.depot.DatabaseException; +import com.samskivert.depot.Transformer; import com.samskivert.depot.annotation.Column; import com.samskivert.depot.annotation.Computed; import com.samskivert.depot.annotation.GeneratedValue; +import com.samskivert.depot.annotation.Transform; import com.samskivert.jdbc.ColumnDefinition; import com.samskivert.util.ByteEnum; import com.samskivert.util.ByteEnumUtil; +import com.samskivert.util.Logger; import com.samskivert.util.StringUtil; /** @@ -49,74 +53,63 @@ import com.samskivert.util.StringUtil; */ public abstract class FieldMarshaller { + /** Used by the {@link SQLBuilder} implementations. We factor this into an interface to avoid a + * bunch of instanceof casts and to ensure that if a new supported type is added, all of the + * builders will fail to compile instead of failing at runtime. */ + public static interface ColumnTyper { + String getBooleanType (int length); + String getByteType (int length); + String getShortType (int length); + String getIntType (int length); + String getLongType (int length); + String getFloatType (int length); + String getDoubleType (int length); + String getStringType (int length); + String getDateType (int length); + String getTimeType (int length); + String getTimestampType (int length); + String getBlobType (int length); + String getClobType (int length); + } + /** * Creates and returns a field marshaller for the specified field. Throws an exception if the * field in question cannot be marshalled. */ public static FieldMarshaller createMarshaller (Field field) { - Class ftype = field.getType(); - FieldMarshaller marshaller; - - // primitive types - if (ftype.equals(Boolean.TYPE)) { - marshaller = new BooleanMarshaller(); - } else if (ftype.equals(Byte.TYPE)) { - marshaller = new ByteMarshaller(); - } else if (ftype.equals(Short.TYPE)) { - marshaller = new ShortMarshaller(); - } else if (ftype.equals(Integer.TYPE)) { - marshaller = new IntMarshaller(); - } else if (ftype.equals(Long.TYPE)) { - marshaller = new LongMarshaller(); - } else if (ftype.equals(Float.TYPE)) { - marshaller = new FloatMarshaller(); - } else if (ftype.equals(Double.TYPE)) { - marshaller = new DoubleMarshaller(); - - // "natural" types - } else if (ftype.equals(Byte.class) || - ftype.equals(Short.class) || - ftype.equals(Integer.class) || - ftype.equals(Long.class) || - ftype.equals(Float.class) || - ftype.equals(Double.class) || - ftype.equals(String.class)) { - marshaller = new ObjectMarshaller(); - - // some primitive array types - } else if (ftype.equals(byte[].class)) { - marshaller = new ByteArrayMarshaller(); - - } else if (ftype.equals(int[].class)) { - marshaller = new IntArrayMarshaller(); - - // SQL types - } else if (ftype.equals(Date.class) || - ftype.equals(Time.class) || - ftype.equals(Timestamp.class) || - ftype.equals(Blob.class) || - ftype.equals(Clob.class)) { - marshaller = new ObjectMarshaller(); - - // special Enum types - } else if (ByteEnum.class.isAssignableFrom(ftype)) { - // do some sanity checking so that the unsafe business we do below is safer - if (!Enum.class.isAssignableFrom(ftype)) { - throw new IllegalArgumentException( - "ByteEnum not implemented by real Enum: " + field); + // first, look for a @Transform annotation on the field (cheap) + Transform xform = field.getAnnotation(Transform.class); + if (xform == null) { + // next look for a marshaller for the basic type (cheapish) + FieldMarshaller marshaller = createMarshaller(field.getType()); + if (marshaller != null) { + marshaller.create(field); + return marshaller; } - @SuppressWarnings("unchecked") ByteEnumMarshaller bem = - new ByteEnumMarshaller(ftype); - marshaller = bem; - } else { - throw new IllegalArgumentException( - "Cannot marshall field of type '" + ftype.getName() + "'."); + // finally look for an @Transform annotation on the field type (expensive) + xform = findTransformAnnotation(field.getType()); + if (xform == null) { + throw new IllegalArgumentException("Cannot marshall " + field + "."); + } } - marshaller.create(field); - return marshaller; + try { + @SuppressWarnings("unchecked") Transformer xformer = xform.value().newInstance(); + @SuppressWarnings("unchecked") TransformingMarshaller xmarsh = + new TransformingMarshaller(xformer, field); + xmarsh.create(field); + return xmarsh; + } catch (InstantiationException e) { + throw new IllegalArgumentException( + Logger.format("Unable to create Transformer", "xclass", xform.value(), + "field", field), e); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException( + Logger.format("Unable to create Transformer", "xclass", xform.value(), + "field", field), e); + } } /** @@ -169,6 +162,12 @@ public abstract class FieldMarshaller return _columnDefinition; } + /** + * Returns the appropriate column type for this field, given the database specific typer + * supplied as an argument. + */ + public abstract String getColumnType (ColumnTyper typer, int length); + /** * Reads this field from the given persistent object. */ @@ -234,7 +233,172 @@ public abstract class FieldMarshaller _generatedValue = field.getAnnotation(GeneratedValue.class); } + protected static FieldMarshaller createMarshaller (Class ftype) + { + // primitive types + if (ftype.equals(Boolean.TYPE)) { + return new BooleanMarshaller(); + } else if (ftype.equals(Byte.TYPE)) { + return new ByteMarshaller(); + } else if (ftype.equals(Short.TYPE)) { + return new ShortMarshaller(); + } else if (ftype.equals(Integer.TYPE)) { + return new IntMarshaller(); + } else if (ftype.equals(Long.TYPE)) { + return new LongMarshaller(); + } else if (ftype.equals(Float.TYPE)) { + return new FloatMarshaller(); + } else if (ftype.equals(Double.TYPE)) { + return new DoubleMarshaller(); + + // "natural" types + } else if (ftype.equals(Byte.class)) { + return new ObjectMarshaller() { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getByteType(length); + } + @Override public Object getFromSet (ResultSet rs) + throws SQLException { + Object value = super.getFromSet(rs); + return (value == null) ? null : ((Number)value).byteValue(); + } + }; + } else if (ftype.equals(Short.class)) { + return new ObjectMarshaller() { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getShortType(length); + } + @Override public Object getFromSet (ResultSet rs) + throws SQLException { + Object value = super.getFromSet(rs); + return (value == null) ? null : ((Number)value).shortValue(); + } + }; + } else if (ftype.equals(Integer.class)) { + return new ObjectMarshaller() { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getIntType(length); + } + }; + } else if (ftype.equals(Long.class)) { + return new ObjectMarshaller() { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getLongType(length); + } + }; + } else if (ftype.equals(Float.class)) { + return new ObjectMarshaller() { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getFloatType(length); + } + @Override public Object getFromSet (ResultSet rs) + throws SQLException { + Object value = super.getFromSet(rs); + return (value == null) ? null : ((Number)value).floatValue(); + } + }; + } else if (ftype.equals(Double.class)) { + return new ObjectMarshaller() { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getDoubleType(length); + } + }; + } else if (ftype.equals(String.class)) { + return new ObjectMarshaller() { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getStringType(length); + } + }; + + // some primitive array types + } else if (ftype.equals(byte[].class)) { + return new ByteArrayMarshaller(); + + } else if (ftype.equals(int[].class)) { + return new IntArrayMarshaller(); + + // SQL types + } else if (ftype.equals(Date.class)) { + return new ObjectMarshaller() { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getDateType(length); + } + }; + } else if (ftype.equals(Time.class)) { + return new ObjectMarshaller() { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getTimeType(length); + } + }; + } else if (ftype.equals(Timestamp.class)) { + return new ObjectMarshaller() { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getTimestampType(length); + } + }; + } else if (ftype.equals(Blob.class)) { + return new ObjectMarshaller() { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getBlobType(length); + } + }; + } else if (ftype.equals(Clob.class)) { + return new ObjectMarshaller() { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getClobType(length); + } + }; + + // special Enum type hackery (it would be nice to handle this with @Transform, but that + // introduces some undesirable samskivert-Depot dependencies) + } else if (ByteEnum.class.isAssignableFrom(ftype)) { + @SuppressWarnings("unchecked") ByteEnumMarshaller bem = + new ByteEnumMarshaller(ftype); + return bem; + + } else { + return null; + } + } + + protected static Class getTransformerType (Transformer xformer, String which) + { + Class ttype = null; + for (Method method : xformer.getClass().getDeclaredMethods()) { + if (method.getName().equals(which + "Persistent")) { + if (ttype == null || ttype.isAssignableFrom(method.getReturnType())) { + ttype = method.getReturnType(); + } + } + } + if (ttype == null) { + throw new IllegalArgumentException( + Logger.format("Transformer lacks " + which + "Persistent() method!?", + "xclass", xformer.getClass())); + } + return ttype; + } + + protected static Transform findTransformAnnotation (Class ftype) + { + Transform xform = ftype.getAnnotation(Transform.class); + if (xform != null) { + return xform; + } + for (Class iface : ftype.getInterfaces()) { + xform = iface.getAnnotation(Transform.class); + if (xform != null) { + return xform; + } + } + Class parent = ftype.getSuperclass(); + return (parent == null) ? null : findTransformAnnotation(parent); + } + protected static class BooleanMarshaller extends FieldMarshaller { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getBooleanType(length); + } @Override public Boolean getFromObject (Object po) throws IllegalArgumentException, IllegalAccessException { return _field.getBoolean(po); @@ -254,6 +418,9 @@ public abstract class FieldMarshaller } protected static class ByteMarshaller extends FieldMarshaller { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getByteType(length); + } @Override public Byte getFromObject (Object po) throws IllegalArgumentException, IllegalAccessException { return _field.getByte(po); @@ -273,6 +440,9 @@ public abstract class FieldMarshaller } protected static class ShortMarshaller extends FieldMarshaller { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getShortType(length); + } @Override public Short getFromObject (Object po) throws IllegalArgumentException, IllegalAccessException { return _field.getShort(po); @@ -292,6 +462,9 @@ public abstract class FieldMarshaller } protected static class IntMarshaller extends FieldMarshaller { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getIntType(length); + } @Override public Integer getFromObject (Object po) throws IllegalArgumentException, IllegalAccessException { return _field.getInt(po); @@ -311,6 +484,9 @@ public abstract class FieldMarshaller } protected static class LongMarshaller extends FieldMarshaller { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getLongType(length); + } @Override public Long getFromObject (Object po) throws IllegalArgumentException, IllegalAccessException { return _field.getLong(po); @@ -330,6 +506,9 @@ public abstract class FieldMarshaller } protected static class FloatMarshaller extends FieldMarshaller { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getFloatType(length); + } @Override public Float getFromObject (Object po) throws IllegalArgumentException, IllegalAccessException { return _field.getFloat(po); @@ -349,6 +528,9 @@ public abstract class FieldMarshaller } protected static class DoubleMarshaller extends FieldMarshaller { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getDoubleType(length); + } @Override public Double getFromObject (Object po) throws IllegalArgumentException, IllegalAccessException { return _field.getDouble(po); @@ -367,7 +549,7 @@ public abstract class FieldMarshaller } } - protected static class ObjectMarshaller extends FieldMarshaller { + protected static abstract class ObjectMarshaller extends FieldMarshaller { @Override public Object getFromObject (Object po) throws IllegalArgumentException, IllegalAccessException { return _field.get(po); @@ -387,6 +569,9 @@ public abstract class FieldMarshaller } protected static class ByteArrayMarshaller extends FieldMarshaller { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getBlobType(length); + } @Override public byte[] getFromObject (Object po) throws IllegalArgumentException, IllegalAccessException { return (byte[]) _field.get(po); @@ -406,6 +591,9 @@ public abstract class FieldMarshaller } protected static class IntArrayMarshaller extends FieldMarshaller { + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getBlobType(length); + } @Override public byte[] getFromObject (Object po) throws IllegalArgumentException, IllegalAccessException { int[] values = (int[]) _field.get(po); @@ -441,6 +629,18 @@ public abstract class FieldMarshaller _eclass = clazz; } + @Override public void create (Field field) { + super.create(field); + // do some sanity checking so that the unsafe business we do below is safer + if (!Enum.class.isAssignableFrom(_eclass)) { + throw new IllegalArgumentException( + "ByteEnum not implemented by real Enum: " + field); + } + } + + @Override public String getColumnType (ColumnTyper typer, int length) { + return typer.getByteType(length); + } @Override public ByteEnum getFromObject (Object po) throws IllegalArgumentException, IllegalAccessException { return (ByteEnum) _field.get(po); @@ -460,6 +660,50 @@ public abstract class FieldMarshaller protected Class _eclass; } + protected static class TransformingMarshaller extends FieldMarshaller { + public TransformingMarshaller (Transformer xformer, Field field) { + Class pojoType = getTransformerType(xformer, "from"); + if (!pojoType.isAssignableFrom(field.getType())) { + throw new IllegalArgumentException( + "@Transform error on " + field.getType().getName() + "." + + field.getName() + ": " + xformer.getClass().getName() + " cannot convert " + + field.getType().getName()); + } + @SuppressWarnings("unchecked") FieldMarshaller delegate = + (FieldMarshaller)createMarshaller(getTransformerType(xformer, "to")); + _delegate = delegate; + _xformer = xformer; + } + + @Override public void create (Field field) { + super.create(field); + _delegate.create(field); + } + + @Override public String getColumnType (ColumnTyper typer, int length) { + return _delegate.getColumnType(typer, length); + } + @Override public T getFromObject (Object po) + throws IllegalArgumentException, IllegalAccessException { + @SuppressWarnings("unchecked") F value = (F)_field.get(po); + return _xformer.toPersistent(value); + } + @Override public T getFromSet (ResultSet rs) throws SQLException { + return _delegate.getFromSet(rs); + } + @Override public void writeToObject (Object po, T value) + throws IllegalArgumentException, IllegalAccessException { + _field.set(po, _xformer.fromPersistent(_field.getType(), value)); + } + @Override public void writeToStatement (PreparedStatement ps, int column, T value) + throws SQLException { + _delegate.writeToStatement(ps, column, value); + } + + protected Transformer _xformer; + protected FieldMarshaller _delegate; + } + protected Field _field; protected String _columnName; protected ColumnDefinition _columnDefinition;