From fec4f593ccac17493f0014bd8fabdedc278135c8 Mon Sep 17 00:00:00 2001
From: "Brian S. O'Neill" <bronee@gmail.com>
Date: Tue, 22 Jul 2008 09:19:51 +0000
Subject: Added advanced conversion capability when setting query filter
 properties. This is used to ensure that BigDecimal values are properly
 normalized.

---
 .../com/amazon/carbonado/filter/FilterValues.java  |  29 +-
 .../amazon/carbonado/filter/PropertyFilter.java    | 214 +++-----
 .../com/amazon/carbonado/gen/MasterFeature.java    |   3 +
 .../carbonado/gen/MasterStorableGenerator.java     | 137 ++++-
 .../amazon/carbonado/gen/StorableGenerator.java    |   4 +-
 .../carbonado/info/ConversionComparator.java       | 213 --------
 .../carbonado/info/StorableIntrospector.java       |   1 +
 .../carbonado/raw/GenericEncodingStrategy.java     | 128 ++---
 .../amazon/carbonado/raw/GenericStorableCodec.java |  55 +-
 .../amazon/carbonado/raw/RawStorableGenerator.java |   4 +-
 .../com/amazon/carbonado/repo/map/MapStorage.java  |   4 +-
 .../carbonado/util/ConversionComparator.java       | 213 ++++++++
 .../java/com/amazon/carbonado/util/Converter.java  | 602 +++++++++++++++++++++
 13 files changed, 1109 insertions(+), 498 deletions(-)
 delete mode 100644 src/main/java/com/amazon/carbonado/info/ConversionComparator.java
 create mode 100644 src/main/java/com/amazon/carbonado/util/ConversionComparator.java
 create mode 100644 src/main/java/com/amazon/carbonado/util/Converter.java

(limited to 'src/main/java/com/amazon')

diff --git a/src/main/java/com/amazon/carbonado/filter/FilterValues.java b/src/main/java/com/amazon/carbonado/filter/FilterValues.java
index 6e7202c..a56fc22 100644
--- a/src/main/java/com/amazon/carbonado/filter/FilterValues.java
+++ b/src/main/java/com/amazon/carbonado/filter/FilterValues.java
@@ -99,7 +99,7 @@ public class FilterValues<S extends Storable> implements Serializable, Appender
         try {
             obj = current.getPropertyFilter().adaptValue(value);
         } catch (IllegalArgumentException e) {
-            throw mismatch(int.class, value);
+            throw mismatch(e);
         }
         return with(current, obj);
     }
@@ -117,7 +117,7 @@ public class FilterValues<S extends Storable> implements Serializable, Appender
         try {
             obj = current.getPropertyFilter().adaptValue(value);
         } catch (IllegalArgumentException e) {
-            throw mismatch(long.class, value);
+            throw mismatch(e);
         }
         return with(current, obj);
     }
@@ -135,7 +135,7 @@ public class FilterValues<S extends Storable> implements Serializable, Appender
         try {
             obj = current.getPropertyFilter().adaptValue(value);
         } catch (IllegalArgumentException e) {
-            throw mismatch(float.class, value);
+            throw mismatch(e);
         }
         return with(current, obj);
     }
@@ -153,7 +153,7 @@ public class FilterValues<S extends Storable> implements Serializable, Appender
         try {
             obj = current.getPropertyFilter().adaptValue(value);
         } catch (IllegalArgumentException e) {
-            throw mismatch(double.class, value);
+            throw mismatch(e);
         }
         return with(current, obj);
     }
@@ -171,7 +171,7 @@ public class FilterValues<S extends Storable> implements Serializable, Appender
         try {
             obj = current.getPropertyFilter().adaptValue(value);
         } catch (IllegalArgumentException e) {
-            throw mismatch(boolean.class, value);
+            throw mismatch(e);
         }
         return with(current, obj);
     }
@@ -189,7 +189,7 @@ public class FilterValues<S extends Storable> implements Serializable, Appender
         try {
             obj = current.getPropertyFilter().adaptValue(value);
         } catch (IllegalArgumentException e) {
-            throw mismatch(char.class, value);
+            throw mismatch(e);
         }
         return with(current, obj);
     }
@@ -207,7 +207,7 @@ public class FilterValues<S extends Storable> implements Serializable, Appender
         try {
             obj = current.getPropertyFilter().adaptValue(value);
         } catch (IllegalArgumentException e) {
-            throw mismatch(byte.class, value);
+            throw mismatch(e);
         }
         return with(current, obj);
     }
@@ -225,7 +225,7 @@ public class FilterValues<S extends Storable> implements Serializable, Appender
         try {
             obj = current.getPropertyFilter().adaptValue(value);
         } catch (IllegalArgumentException e) {
-            throw mismatch(short.class, value);
+            throw mismatch(e);
         }
         return with(current, obj);
     }
@@ -243,7 +243,7 @@ public class FilterValues<S extends Storable> implements Serializable, Appender
         try {
             obj = current.getPropertyFilter().adaptValue(value);
         } catch (IllegalArgumentException e) {
-            throw mismatch(value == null ? null : value.getClass(), value);
+            throw mismatch(e);
         }
         return with(current, obj);
     }
@@ -672,17 +672,12 @@ public class FilterValues<S extends Storable> implements Serializable, Appender
         return current;
     }
 
-    private IllegalArgumentException mismatch(Class<?> actualType, Object actualValue) {
+    private IllegalArgumentException mismatch(IllegalArgumentException e) {
         PropertyFilterList<S> current = currentProperty();
         PropertyFilter<S> propFilter = current.getPropertyFilter();
 
         StringBuilder b = new StringBuilder();
-
-        try {
-            propFilter.appendMismatchMessage(b, actualType, actualValue);
-        } catch (IOException e) {
-            // Not gonna happen
-        }
+        b.append(e.getMessage());
 
         int subFilterCount = current.getPreviousRemaining() + current.getNextRemaining() + 1;
 
@@ -696,7 +691,7 @@ public class FilterValues<S extends Storable> implements Serializable, Appender
             b.append(" sub filter in \"");
             try {
                 appendTo(b);
-            } catch (IOException e) {
+            } catch (IOException e2) {
                 // Not gonna happen
             }
             b.append('"');
diff --git a/src/main/java/com/amazon/carbonado/filter/PropertyFilter.java b/src/main/java/com/amazon/carbonado/filter/PropertyFilter.java
index 5014733..2cecca3 100644
--- a/src/main/java/com/amazon/carbonado/filter/PropertyFilter.java
+++ b/src/main/java/com/amazon/carbonado/filter/PropertyFilter.java
@@ -19,6 +19,10 @@
 package com.amazon.carbonado.filter;
 
 import java.io.IOException;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
 import java.util.Collections;
 import java.util.List;
 
@@ -28,6 +32,8 @@ import com.amazon.carbonado.Storable;
 import com.amazon.carbonado.info.ChainedProperty;
 import com.amazon.carbonado.info.StorableProperty;
 
+import com.amazon.carbonado.util.Converter;
+
 /**
  * Filter tree node that performs a relational test against a specific property
  * value.
@@ -38,7 +44,13 @@ public class PropertyFilter<S extends Storable> extends Filter<S> {
     private static final long serialVersionUID = 1L;
 
     // Indicates property has been bound to a constant value.
-    private static int BOUND_CONSTANT = -1;
+    private static final int BOUND_CONSTANT = -1;
+
+    private static final Converter cConverter;
+
+    static {
+        cConverter = Converter.build(Hidden.Adapter.class);
+    }
 
     /**
      * Returns a canonical instance, creating a new one if there isn't one
@@ -334,171 +346,63 @@ public class PropertyFilter<S extends Storable> extends Filter<S> {
      * @throws IllegalArgumentException if type doesn't match
      */
     Object adaptValue(int value) {
-        Class<?> type = getBoxedType();
-        if (type == Integer.class) {
-            return Integer.valueOf(value);
-        } else if (type == Long.class) {
-            return Long.valueOf(value);
-        } else if (type == Double.class) {
-            return Double.valueOf(value);
-        } else if (type == Number.class || type == Object.class) {
-            return Integer.valueOf(value);
-        }
-        throw mismatch(int.class, value);
+        return cConverter.convert(value, getType());
     }
 
     /**
      * @throws IllegalArgumentException if type doesn't match
      */
     Object adaptValue(long value) {
-        Class<?> type = getBoxedType();
-        if (type == Long.class) {
-            return Long.valueOf(value);
-        } else if (type == Number.class || type == Object.class) {
-            return Long.valueOf(value);
-        }
-        throw mismatch(long.class, value);
+        return cConverter.convert(value, getType());
     }
 
     /**
      * @throws IllegalArgumentException if type doesn't match
      */
     Object adaptValue(float value) {
-        Class<?> type = getBoxedType();
-        if (type == Float.class) {
-            return Float.valueOf(value);
-        } else if (type == Double.class) {
-            return Double.valueOf(value);
-        } else if (type == Number.class || type == Object.class) {
-            return Float.valueOf(value);
-        }
-        throw mismatch(float.class, value);
+        return cConverter.convert(value, getType());
     }
 
     /**
      * @throws IllegalArgumentException if type doesn't match
      */
     Object adaptValue(double value) {
-        Class<?> type = getBoxedType();
-        if (type == Double.class) {
-            return Double.valueOf(value);
-        } else if (type == Number.class || type == Object.class) {
-            return Double.valueOf(value);
-        }
-        throw mismatch(float.class, value);
+        return cConverter.convert(value, getType());
     }
 
     /**
      * @throws IllegalArgumentException if type doesn't match
      */
     Object adaptValue(boolean value) {
-        Class<?> type = getBoxedType();
-        if (type == Boolean.class || type == Object.class) {
-            return Boolean.valueOf(value);
-        }
-        throw mismatch(boolean.class, value);
+        return cConverter.convert(value, getType());
     }
 
     /**
      * @throws IllegalArgumentException if type doesn't match
      */
     Object adaptValue(char value) {
-        Class<?> type = getBoxedType();
-        if (type == Character.class || type == Object.class) {
-            return Character.valueOf(value);
-        } else if (type == String.class || type == CharSequence.class) {
-            return String.valueOf(value);
-        }
-        throw mismatch(char.class, value);
+        return cConverter.convert(value, getType());
     }
 
     /**
      * @throws IllegalArgumentException if type doesn't match
      */
     Object adaptValue(byte value) {
-        Class<?> type = getBoxedType();
-        if (type == Byte.class) {
-            return Byte.valueOf(value);
-        } else if (type == Short.class) {
-            return Short.valueOf(value);
-        } else if (type == Integer.class) {
-            return Integer.valueOf(value);
-        } else if (type == Long.class) {
-            return Long.valueOf(value);
-        } else if (type == Double.class) {
-            return Double.valueOf(value);
-        } else if (type == Float.class) {
-            return Float.valueOf(value);
-        } else if (type == Number.class || type == Object.class) {
-            return Byte.valueOf(value);
-        }
-        throw mismatch(byte.class, value);
+        return cConverter.convert(value, getType());
     }
 
     /**
      * @throws IllegalArgumentException if type doesn't match
      */
     Object adaptValue(short value) {
-        Class<?> type = getBoxedType();
-        if (type == Short.class) {
-            return Short.valueOf(value);
-        } else if (type == Integer.class) {
-            return Integer.valueOf(value);
-        } else if (type == Long.class) {
-            return Long.valueOf(value);
-        } else if (type == Double.class) {
-            return Double.valueOf(value);
-        } else if (type == Float.class) {
-            return Float.valueOf(value);
-        } else if (type == Number.class || type == Object.class) {
-            return Short.valueOf(value);
-        }
-        throw mismatch(short.class, value);
+        return cConverter.convert(value, Object.class);
     }
 
     /**
      * @throws IllegalArgumentException if type doesn't match
      */
     Object adaptValue(Object value) {
-        if (getBoxedType().isInstance(value)) {
-            return value;
-        }
-
-        Class<?> type = getType();
-
-        if (value == null) {
-            if (!type.isPrimitive()) {
-                return value;
-            }
-        } else if (type.isPrimitive()) {
-            TypeDesc actualPrim = TypeDesc.forClass(value.getClass()).toPrimitiveType();
-            if (actualPrim != null) {
-                if (type == actualPrim.toClass()) {
-                    return value;
-                }
-                // Unbox and rebox.
-                switch (actualPrim.getTypeCode()) {
-                case TypeDesc.BYTE_CODE:
-                    return adaptValue(((Number) value).byteValue());
-                case TypeDesc.SHORT_CODE:
-                    return adaptValue(((Number) value).shortValue());
-                case TypeDesc.INT_CODE:
-                    return adaptValue(((Number) value).intValue());
-                case TypeDesc.LONG_CODE:
-                    return adaptValue(((Number) value).longValue());
-                case TypeDesc.FLOAT_CODE:
-                    return adaptValue(((Number) value).floatValue());
-                case TypeDesc.DOUBLE_CODE:
-                    return adaptValue(((Number) value).doubleValue());
-                case TypeDesc.BOOLEAN_CODE:
-                    return adaptValue(((Boolean) value).booleanValue());
-                case TypeDesc.CHAR_CODE:
-                    return adaptValue(((Character) value).charValue());
-                }
-            }
-        }
-
-        throw mismatch(value == null ? null : value.getClass(), value);
+        return cConverter.convert(value, getType());
     }
 
     @Override
@@ -546,29 +450,59 @@ public class PropertyFilter<S extends Storable> extends Filter<S> {
         }
     }
 
-    void appendMismatchMessage(Appendable a, Class<?> actualType, Object actualValue)
-        throws IOException
-    {
-        if (actualType == null || actualValue == null) {
-            a.append("Actual value is null, which cannot be assigned to type \"");
-        } else {
-            a.append("Actual value \"");
-            a.append(String.valueOf(actualValue));
-            a.append("\", of type \"");
-            a.append(TypeDesc.forClass(actualType).getFullName());
-            a.append("\", is incompatible with expected type of \"");
-        }
-        a.append(TypeDesc.forClass(getType()).getFullName());
-        a.append('"');
-    }
+    private static class Hidden {
+        public static abstract class Adapter extends Converter {
+            public String convertToString(char value) {
+                return String.valueOf(value);
+            }
+
+            public CharSequence convertToCharSequence(char value) {
+                return String.valueOf(value);
+            }
+
+            public String convertToString(StringBuffer value) {
+                return value.toString();
+            }
+
+            public String convertToString(StringBuilder value) {
+                return value.toString();
+            }
+
+            public BigInteger convertToBigInteger(long value) {
+                return BigInteger.valueOf(value);
+            }
 
-    private IllegalArgumentException mismatch(Class<?> actualType, Object actualValue) {
-        StringBuilder b = new StringBuilder();
-        try {
-            appendMismatchMessage(b, actualType, actualValue);
-        } catch (IOException e) {
-            // Not gonna happen
+            public BigDecimal convertToBigDecimal(long value) {
+                if (value > -10 && value < 10) {
+                    return BigDecimal.valueOf(value);
+                }
+                // Normalize value.
+                return BigDecimal.valueOf(value).stripTrailingZeros();
+            }
+
+            public BigDecimal convertToBigDecimal(double value) {
+                if (value == 0) {
+                    return BigDecimal.ZERO;
+                }
+                // Normalize value.
+                return BigDecimal.valueOf(value).stripTrailingZeros();
+            }
+
+            public BigDecimal convertToBigDecimal(BigInteger value) {
+                if (BigInteger.ZERO.equals(value)) {
+                    return BigDecimal.ZERO;
+                }
+                // Normalize value.
+                return new BigDecimal(value, 0).stripTrailingZeros();
+            }
+
+            public BigDecimal convertToBigDecimal(BigDecimal value) {
+                if (value.compareTo(BigDecimal.ZERO) == 0) {
+                    return BigDecimal.ZERO;
+                }
+                // Normalize value.
+                return value.stripTrailingZeros();
+            }
         }
-        return new IllegalArgumentException(b.toString());
     }
 }
diff --git a/src/main/java/com/amazon/carbonado/gen/MasterFeature.java b/src/main/java/com/amazon/carbonado/gen/MasterFeature.java
index a1bb278..f433ce6 100644
--- a/src/main/java/com/amazon/carbonado/gen/MasterFeature.java
+++ b/src/main/java/com/amazon/carbonado/gen/MasterFeature.java
@@ -28,6 +28,9 @@ public enum MasterFeature {
     /** Insert and update operations implement record versioning, if version property exists */
     VERSIONING,
 
+    /** Insert and update operations normalize property types such as BigDecimal */
+    NORMALIZE,
+
     /** Update operations load clean copy first, to prevent destructive update */
     UPDATE_FULL,
 
diff --git a/src/main/java/com/amazon/carbonado/gen/MasterStorableGenerator.java b/src/main/java/com/amazon/carbonado/gen/MasterStorableGenerator.java
index d8d2901..092eea8 100644
--- a/src/main/java/com/amazon/carbonado/gen/MasterStorableGenerator.java
+++ b/src/main/java/com/amazon/carbonado/gen/MasterStorableGenerator.java
@@ -19,8 +19,13 @@
 package com.amazon.carbonado.gen;
 
 import java.lang.reflect.Method;
+
+import java.math.BigDecimal;
+
+import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 
 import org.cojen.classfile.ClassFile;
@@ -489,9 +494,19 @@ public final class MasterStorableGenerator<S extends Storable> {
                 isInitialized.setLocation();
             }
 
+            // Copy of properties before normalization.
+            List<PropertyCopy> unnormalized = null;
+            if (mFeatures.contains(MasterFeature.NORMALIZE)) {
+                unnormalized = addNormalization(b, false);
+            }
+
+            Label doTryStart = b.createLabel().setLocation();
+
             b.loadThis();
             b.invokeVirtual(DO_TRY_INSERT_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null);
 
+            addNormalizationRollback(b, doTryStart, unnormalized);
+
             if (tryStart == null) {
                 b.returnValue(TypeDesc.BOOLEAN);
             } else {
@@ -520,6 +535,7 @@ public final class MasterStorableGenerator<S extends Storable> {
             CodeBuilder b = new CodeBuilder(mi);
 
             if ((!mFeatures.contains(MasterFeature.VERSIONING)) &&
+                (!mFeatures.contains(MasterFeature.NORMALIZE)) &&
                 (!mFeatures.contains(MasterFeature.UPDATE_FULL)) &&
                 (!mFeatures.contains(MasterFeature.UPDATE_TXN)))
             {
@@ -539,7 +555,22 @@ public final class MasterStorableGenerator<S extends Storable> {
 
             Label tryLoadStart = null, tryLoadEnd = null;
 
-            if (mFeatures.contains(MasterFeature.UPDATE_FULL)) {
+            // Copy of properties before normalization.
+            List<PropertyCopy> unnormalized = null;
+            if (mFeatures.contains(MasterFeature.NORMALIZE)) {
+                unnormalized = addNormalization(b, false);
+            }
+
+            Label doTryStart = b.createLabel().setLocation();
+
+            if (!mFeatures.contains(MasterFeature.UPDATE_FULL)) {
+                // if (!this.doTryUpdateMaster()) {
+                //     goto failed;
+                // }
+                b.loadThis();
+                b.invokeVirtual(DO_TRY_UPDATE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null);
+                b.ifZeroComparisonBranch(failed, "==");
+            } else {
                 // Storable saved = copy();
                 b.loadThis();
                 b.invokeVirtual(COPY_METHOD_NAME, storableType, null);
@@ -701,13 +732,6 @@ public final class MasterStorableGenerator<S extends Storable> {
                 b.loadThis();
                 b.invokeInterface
                     (storableType, COPY_UNEQUAL_PROPERTIES, null, new TypeDesc[] {storableType});
-            } else {
-                // if (!this.doTryUpdateMaster()) {
-                //     goto failed;
-                // }
-                b.loadThis();
-                b.invokeVirtual(DO_TRY_UPDATE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null);
-                b.ifZeroComparisonBranch(failed, "==");
             }
 
             // txn.commit();
@@ -1023,4 +1047,101 @@ public final class MasterStorableGenerator<S extends Storable> {
 
         b.invoke(versionProperty.getWriteMethod());
     }
+
+    private List<PropertyCopy> addNormalization(CodeBuilder b, boolean forUpdate) {
+        List<PropertyCopy> unnormalized = null;
+
+        for (StorableProperty<S> property : mAllProperties.values()) {
+            if (property.isDerived()) {
+                continue;
+            }
+            if (!BigDecimal.class.isAssignableFrom(property.getType())) {
+                continue;
+            }
+
+            if (unnormalized == null) {
+                unnormalized = new ArrayList<PropertyCopy>();
+            }
+
+            PropertyCopy copy = new PropertyCopy<S>(b, property);
+            unnormalized.add(copy);
+
+            copy.makeCopy(b);
+
+            b.loadLocal(copy.copyVar);
+            Label skipNormalize = b.createLabel();
+            b.ifNullBranch(skipNormalize, true);
+
+            if (forUpdate) {
+                // FIXME: for update, also check if dirty
+            }
+
+            // Normalize by stripping trailing zeros.
+            // FIXME: Workaround BigDecimal.ZERO bug.
+            b.loadThis();
+            b.loadLocal(copy.copyVar);
+            TypeDesc propertyType = copy.copyVar.getType();
+            b.invokeVirtual(propertyType, "stripTrailingZeros", propertyType, null);
+            b.storeField(property.getName(), propertyType);
+
+            skipNormalize.setLocation();
+        }
+
+        return unnormalized;
+    }
+
+    /**
+     * Assumes a boolean is on the stack, as returned by doTryInsert or doTryUpdate.
+     */
+    private void addNormalizationRollback(CodeBuilder b, Label doTryStart,
+                                          List<PropertyCopy> unnormalized)
+    {
+        if (unnormalized != null) {
+            Label doTryEnd = b.createLabel().setLocation();
+
+            b.dup();
+            Label success = b.createLabel();
+            b.ifZeroComparisonBranch(success, "!=");
+
+            for (int i=0; i<2; i++) {
+                if (i == 0) {
+                } else {
+                    b.exceptionHandler(doTryStart, doTryEnd, null);
+                }
+                // Rollback normalized properties.
+                for (PropertyCopy copy : unnormalized) {
+                    copy.restore(b);
+                }
+                if (i == 0) {
+                    b.branch(success);
+                } else {
+                    b.throwObject();
+                }
+            }
+
+            success.setLocation();
+        }
+    }
+
+    private static class PropertyCopy<S extends Storable> {
+        final StorableProperty<S> property;
+        final LocalVariable copyVar;
+
+        PropertyCopy(CodeBuilder b, StorableProperty<S> property) {
+            this.property = property;
+            copyVar = b.createLocalVariable(null, TypeDesc.forClass(property.getType()));
+        }
+
+        void makeCopy(CodeBuilder b) {
+            b.loadThis();
+            b.loadField(property.getName(), copyVar.getType());
+            b.storeLocal(copyVar);
+        }
+
+        void restore(CodeBuilder b) {
+            b.loadThis();
+            b.loadLocal(copyVar);
+            b.storeField(property.getName(), copyVar.getType());
+        }
+    }
 }
diff --git a/src/main/java/com/amazon/carbonado/gen/StorableGenerator.java b/src/main/java/com/amazon/carbonado/gen/StorableGenerator.java
index 0eee3de..e88ba39 100644
--- a/src/main/java/com/amazon/carbonado/gen/StorableGenerator.java
+++ b/src/main/java/com/amazon/carbonado/gen/StorableGenerator.java
@@ -2732,7 +2732,7 @@ public final class StorableGenerator<S extends Storable> {
 
         LocalVariable encodedVar;
         try {
-            encodedVar = encoder.buildSerialEncoding(b, null, null);
+            encodedVar = encoder.buildSerialEncoding(b, null);
         } catch (SupportException e) {
             CodeBuilderUtil.throwException(b, SupportException.class, e.getMessage());
             return;
@@ -2780,7 +2780,7 @@ public final class StorableGenerator<S extends Storable> {
         GenericEncodingStrategy<S> encoder = new GenericEncodingStrategy<S>(mStorableType, null);
 
         try {
-            encoder.buildSerialDecoding(b, null, null, encodedVar);
+            encoder.buildSerialDecoding(b, null, encodedVar);
         } catch (SupportException e) {
             CodeBuilderUtil.throwException(b, SupportException.class, e.getMessage());
             return;
diff --git a/src/main/java/com/amazon/carbonado/info/ConversionComparator.java b/src/main/java/com/amazon/carbonado/info/ConversionComparator.java
deleted file mode 100644
index 0feac47..0000000
--- a/src/main/java/com/amazon/carbonado/info/ConversionComparator.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
- * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
- * of Amazon Technologies, Inc. or its affiliates.  All rights reserved.
- *
- * 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.amazon.carbonado.info;
-
-import java.util.Comparator;
-
-import org.cojen.classfile.TypeDesc;
-
-/**
- * Compares type conversions, finding the one that is nearest.
- *
- * @author Brian S O'Neill
- * @since 1.2
- */
-class ConversionComparator implements Comparator<Class> {
-    private final TypeDesc mFrom;
-
-    public ConversionComparator(Class fromType) {
-        mFrom = TypeDesc.forClass(fromType);
-    }
-
-    /**
-     * Returns true if a coversion is possible to the given type.
-     */
-    public boolean isConversionPossible(Class toType) {
-        return isConversionPossible(mFrom, TypeDesc.forClass(toType));
-    }
-
-    @SuppressWarnings("unchecked")
-    private static boolean isConversionPossible(TypeDesc from, TypeDesc to) {
-        if (from == to) {
-            return true;
-        }
-
-        if (from.toPrimitiveType() != null && to.toPrimitiveType() != null) {
-            from = from.toPrimitiveType();
-            to = to.toPrimitiveType();
-        } else {
-            from = from.toObjectType();
-            to = to.toObjectType();
-        }
-
-        switch (from.getTypeCode()) {
-        case TypeDesc.OBJECT_CODE: default:
-            return to.toClass().isAssignableFrom(from.toClass());
-        case TypeDesc.BOOLEAN_CODE:
-            return to == TypeDesc.BOOLEAN;
-        case TypeDesc.BYTE_CODE:
-            return to == TypeDesc.BYTE || to == TypeDesc.SHORT
-                || to == TypeDesc.INT || to == TypeDesc.LONG
-                || to == TypeDesc.FLOAT || to == TypeDesc.DOUBLE;
-        case TypeDesc.SHORT_CODE:
-            return to == TypeDesc.SHORT
-                || to == TypeDesc.INT || to == TypeDesc.LONG
-                || to == TypeDesc.FLOAT || to == TypeDesc.DOUBLE;
-        case TypeDesc.CHAR_CODE:
-            return to == TypeDesc.CHAR;
-        case TypeDesc.INT_CODE:
-            return to == TypeDesc.INT || to == TypeDesc.LONG || to == TypeDesc.DOUBLE;
-        case TypeDesc.FLOAT_CODE:
-            return to == TypeDesc.FLOAT || to == TypeDesc.DOUBLE;
-        case TypeDesc.LONG_CODE:
-            return to == TypeDesc.LONG;
-        case TypeDesc.DOUBLE_CODE:
-            return to == TypeDesc.DOUBLE;
-        }
-    }
-
-    /**
-     * Evaluates two types, to see which one is nearest to the from type.
-     * Return {@literal <0} if "a" is nearest, 0 if both are equally good,
-     * {@literal >0} if "b" is nearest.
-     */
-    public int compare(Class toType_a, Class toType_b) {
-        TypeDesc from = mFrom;
-        TypeDesc a = TypeDesc.forClass(toType_a);
-        TypeDesc b = TypeDesc.forClass(toType_b);
-
-        if (from == a) {
-            if (from == b) {
-                return 0;
-            }
-            return -1;
-        } else if (from == b) {
-            return 1;
-        }
-
-        int result = compare(from, a, b);
-        if (result != 0) {
-            return result;
-        }
-
-        if (from.isPrimitive()) {
-            // Try boxing.
-            if (from.toObjectType() != null) {
-                from = from.toObjectType();
-                return compare(from, a, b);
-            }
-        } else {
-            // Try unboxing.
-            if (from.toPrimitiveType() != null) {
-                from = from.toPrimitiveType();
-                result = compare(from, a, b);
-                if (result != 0) {
-                    return result;
-                }
-                // Try boxing back up. Test by unboxing 'to' types.
-                if (!toType_a.isPrimitive() && a.toPrimitiveType() != null) {
-                    a = a.toPrimitiveType();
-                }
-                if (!toType_b.isPrimitive() && b.toPrimitiveType() != null) {
-                    b = b.toPrimitiveType();
-                }
-                return compare(from, a, b);
-            }
-        }
-
-        return 0;
-    }
-
-    private static int compare(TypeDesc from, TypeDesc a, TypeDesc b) {
-        if (isConversionPossible(from, a)) {
-            if (isConversionPossible(from, b)) {
-                if (from.isPrimitive()) {
-                    if (a.isPrimitive()) {
-                        if (b.isPrimitive()) {
-                            // Choose the one with the least amount of widening.
-                            return primitiveWidth(a) - primitiveWidth(b);
-                        } else {
-                            return -1;
-                        }
-                    } else if (b.isPrimitive()) {
-                        return 1;
-                    }
-                } else {
-                    // Choose the one with the shortest distance up the class
-                    // hierarchy.
-                    Class fromClass = from.toClass();
-                    if (!fromClass.isInterface()) {
-                        if (a.toClass().isInterface()) {
-                            if (!b.toClass().isInterface()) {
-                                return -1;
-                            }
-                        } else if (b.toClass().isInterface()) {
-                            return 1;
-                        } else {
-                            return distance(fromClass, a.toClass())
-                                - distance(fromClass, b.toClass());
-                        }
-                    }
-                }
-            } else {
-                return -1;
-            }
-        } else if (isConversionPossible(from, b)) {
-            return 1;
-        }
-
-        return 0;
-    }
-
-    // 1 = boolean, 2 = byte, 3 = short, 4 = char, 5 = int, 6 = float, 7 = long, 8 = double
-    private static int primitiveWidth(TypeDesc type) {
-        switch (type.getTypeCode()) {
-        default:
-            return 0;
-        case TypeDesc.BOOLEAN_CODE:
-            return 1;
-        case TypeDesc.BYTE_CODE:
-            return 2;
-        case TypeDesc.SHORT_CODE:
-            return 3;
-        case TypeDesc.CHAR_CODE:
-            return 4;
-        case TypeDesc.INT_CODE:
-            return 5;
-        case TypeDesc.FLOAT_CODE:
-            return 6;
-        case TypeDesc.LONG_CODE:
-            return 7;
-        case TypeDesc.DOUBLE_CODE:
-            return 8;
-        }
-    }
-
-    private static int distance(Class from, Class to) {
-        int distance = 0;
-        while (from != to) {
-            from = from.getSuperclass();
-            if (from == null) {
-                return Integer.MAX_VALUE;
-            }
-            distance++;
-        }
-        return distance;
-    }
-}
diff --git a/src/main/java/com/amazon/carbonado/info/StorableIntrospector.java b/src/main/java/com/amazon/carbonado/info/StorableIntrospector.java
index 80d263a..bd1d52b 100644
--- a/src/main/java/com/amazon/carbonado/info/StorableIntrospector.java
+++ b/src/main/java/com/amazon/carbonado/info/StorableIntrospector.java
@@ -72,6 +72,7 @@ import com.amazon.carbonado.Version;
 import com.amazon.carbonado.adapter.AdapterDefinition;
 import com.amazon.carbonado.constraint.ConstraintDefinition;
 import com.amazon.carbonado.lob.Lob;
+import com.amazon.carbonado.util.ConversionComparator;
 
 /**
  * Supports examination of {@link Storable} types, returning all metadata
diff --git a/src/main/java/com/amazon/carbonado/raw/GenericEncodingStrategy.java b/src/main/java/com/amazon/carbonado/raw/GenericEncodingStrategy.java
index e7a8ff9..c5417c4 100644
--- a/src/main/java/com/amazon/carbonado/raw/GenericEncodingStrategy.java
+++ b/src/main/java/com/amazon/carbonado/raw/GenericEncodingStrategy.java
@@ -25,7 +25,6 @@ import java.math.BigInteger;
 
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 
@@ -69,26 +68,6 @@ import com.amazon.carbonado.info.StorablePropertyAdapter;
  * @author Brian S O'Neill
  */
 public class GenericEncodingStrategy<S extends Storable> {
-    /**
-     * Defines extra encoding options.
-     *
-     * @since 1.2
-     */
-    public static enum Option {
-        /**
-         * Access properties by public methods instead of protected fields.
-         * Option should be used if class being generated doesn't have access
-         * to these fields.
-         */
-        USE_METHODS,
-
-        /**
-         * Property values such as BigDecimal are normalized before being
-         * encoded.
-         */
-        NORMALIZE,
-    }
-
     private static enum Mode { KEY, DATA, SERIAL }
 
     private final Class<S> mType;
@@ -165,7 +144,9 @@ public class GenericEncodingStrategy<S extends Storable> {
      * of a Storable instance.
      * @param adapterInstanceClass class containing static references to
      * adapter instances - defaults to instanceVar
-     * @param options optional encoding options
+     * @param useReadMethods when true, access properties by public read
+     * methods instead of protected fields - should be used if class being
+     * generated doesn't have access to these fields
      * @param partialStartVar optional variable for supporting partial key
      * generation. It must be an int, whose runtime value must be less than the
      * properties array length. It marks the range start of the partial
@@ -186,7 +167,7 @@ public class GenericEncodingStrategy<S extends Storable> {
                                           OrderedProperty<S>[] properties,
                                           LocalVariable instanceVar,
                                           Class<?> adapterInstanceClass,
-                                          EnumSet<Option> options,
+                                          boolean useReadMethods,
                                           LocalVariable partialStartVar,
                                           LocalVariable partialEndVar)
         throws SupportException
@@ -195,7 +176,7 @@ public class GenericEncodingStrategy<S extends Storable> {
         return buildEncoding(Mode.KEY, assembler,
                              extractProperties(properties), extractDirections(properties),
                              instanceVar, adapterInstanceClass,
-                             options,
+                             useReadMethods,
                              -1, // no generation support
                              partialStartVar, partialEndVar);
     }
@@ -213,7 +194,9 @@ public class GenericEncodingStrategy<S extends Storable> {
      * of a Storable instance.
      * @param adapterInstanceClass class containing static references to
      * adapter instances - defaults to instanceVar
-     * @param options optional encoding options
+     * @param useWriteMethods when true, set properties by public write
+     * methods instead of protected fields - should be used if class being
+     * generated doesn't have access to these fields
      * @param encodedVar required variable, which must be a byte array. At
      * runtime, it references an encoded key.
      *
@@ -225,14 +208,14 @@ public class GenericEncodingStrategy<S extends Storable> {
                                  OrderedProperty<S>[] properties,
                                  LocalVariable instanceVar,
                                  Class<?> adapterInstanceClass,
-                                 EnumSet<Option> options,
+                                 boolean useWriteMethods,
                                  LocalVariable encodedVar)
         throws SupportException
     {
         properties = ensureKeyProperties(properties);
         buildDecoding(Mode.KEY, assembler,
                       extractProperties(properties), extractDirections(properties),
-                      instanceVar, adapterInstanceClass, options,
+                      instanceVar, adapterInstanceClass, useWriteMethods,
                       -1, null, // no generation support
                       encodedVar);
     }
@@ -252,7 +235,8 @@ public class GenericEncodingStrategy<S extends Storable> {
      * of a Storable instance.
      * @param adapterInstanceClass class containing static references to
      * adapter instances - defaults to instanceVar
-     * @param options optional encoding options
+     * @param useReadMethods when true, access properties by public read
+     * methods instead of protected fields
      * @param generation when non-negative, write a storable layout generation
      * value in one or four bytes. Generation 0..127 is encoded in one byte, and
      * 128..max is encoded in four bytes, with the most significant bit set.
@@ -267,7 +251,7 @@ public class GenericEncodingStrategy<S extends Storable> {
                                            StorableProperty<S>[] properties,
                                            LocalVariable instanceVar,
                                            Class<?> adapterInstanceClass,
-                                           EnumSet<Option> options,
+                                           boolean useReadMethods,
                                            int generation)
         throws SupportException
     {
@@ -275,7 +259,7 @@ public class GenericEncodingStrategy<S extends Storable> {
         return buildEncoding(Mode.DATA, assembler,
                              properties, null,
                              instanceVar, adapterInstanceClass,
-                             options, generation, null, null);
+                             useReadMethods, generation, null, null);
     }
 
     /**
@@ -291,7 +275,9 @@ public class GenericEncodingStrategy<S extends Storable> {
      * of a Storable instance.
      * @param adapterInstanceClass class containing static references to
      * adapter instances - defaults to instanceVar
-     * @param options optional encoding options
+     * @param useWriteMethods when true, set properties by public write
+     * methods instead of protected fields - should be used if class being
+     * generated doesn't have access to these fields
      * @param generation when non-negative, decoder expects a storable layout
      * generation value to match this value. Otherwise, it throws a
      * CorruptEncodingException.
@@ -311,7 +297,7 @@ public class GenericEncodingStrategy<S extends Storable> {
                                   StorableProperty<S>[] properties,
                                   LocalVariable instanceVar,
                                   Class<?> adapterInstanceClass,
-                                  EnumSet<Option> options,
+                                  boolean useWriteMethods,
                                   int generation,
                                   Label altGenerationHandler,
                                   LocalVariable encodedVar)
@@ -319,7 +305,7 @@ public class GenericEncodingStrategy<S extends Storable> {
     {
         properties = ensureDataProperties(properties);
         buildDecoding(Mode.DATA, assembler, properties, null,
-                      instanceVar, adapterInstanceClass, options,
+                      instanceVar, adapterInstanceClass, useWriteMethods,
                       generation, altGenerationHandler, encodedVar);
     }
 
@@ -330,19 +316,17 @@ public class GenericEncodingStrategy<S extends Storable> {
      * @param assembler code assembler to receive bytecode instructions
      * @param properties specific properties to decode, defaults to all
      * properties if null
-     * @param options optional encoding options
      * @return local variable referencing a byte array with encoded data
      * @throws SupportException if any property type is not supported
      * @since 1.2
      */
     public LocalVariable buildSerialEncoding(CodeAssembler assembler,
-                                             StorableProperty<S>[] properties,
-                                             EnumSet<Option> options)
+                                             StorableProperty<S>[] properties)
         throws SupportException
     {
         properties = ensureAllProperties(properties);
         return buildEncoding
-            (Mode.SERIAL, assembler, properties, null, null, null, options, -1, null, null);
+            (Mode.SERIAL, assembler, properties, null, null, null, false, -1, null, null);
     }
 
     /**
@@ -352,7 +336,6 @@ public class GenericEncodingStrategy<S extends Storable> {
      * @param assembler code assembler to receive bytecode instructions
      * @param properties specific properties to decode, defaults to all
      * properties if null
-     * @param options optional encoding options
      * @param encodedVar required variable, which must be a byte array. At
      * runtime, it references encoded data.
      * @throws SupportException if any property type is not supported
@@ -361,13 +344,12 @@ public class GenericEncodingStrategy<S extends Storable> {
      */
     public void buildSerialDecoding(CodeAssembler assembler,
                                     StorableProperty<S>[] properties,
-                                    EnumSet<Option> options,
                                     LocalVariable encodedVar)
         throws SupportException
     {
         properties = ensureAllProperties(properties);
         buildDecoding
-            (Mode.SERIAL, assembler, properties, null, null, null, options, -1, null, encodedVar);
+            (Mode.SERIAL, assembler, properties, null, null, null, false, -1, null, encodedVar);
     }
 
     /**
@@ -636,7 +618,7 @@ public class GenericEncodingStrategy<S extends Storable> {
                                         Direction[] directions,
                                         LocalVariable instanceVar,
                                         Class<?> adapterInstanceClass,
-                                        EnumSet<Option> options,
+                                        boolean useReadMethods,
                                         int generation,
                                         LocalVariable partialStartVar,
                                         LocalVariable partialEndVar)
@@ -715,7 +697,7 @@ public class GenericEncodingStrategy<S extends Storable> {
                 // property is optional, then a byte prefix is needed to
                 // identify a null reference.
 
-                loadPropertyValue(a, info, 0, options,
+                loadPropertyValue(a, info, 0, useReadMethods,
                                   instanceVar, adapterInstanceClass, partialStartVar);
 
                 boolean descending = mode == Mode.KEY
@@ -803,10 +785,7 @@ public class GenericEncodingStrategy<S extends Storable> {
                         hasVariableLength = true;
                     }
 
-                    if (info.getPropertyType() == info.getStorageType() &&
-                        // BigDecimal is adapted in this method, to strip trailing zeros.
-                        info.getPropertyType() != TypeDesc.forClass(BigDecimal.class))
-                    {
+                    if (info.getPropertyType() == info.getStorageType()) {
                         // Property won't be adapted, so loading it twice is no big deal.
                         continue;
                     }
@@ -961,7 +940,7 @@ public class GenericEncodingStrategy<S extends Storable> {
                     } else {
                         // Load property to test for null.
                         loadPropertyValue(stashedProperties, stashedFromInstances,
-                                          a, info, i, options,
+                                          a, info, i, useReadMethods,
                                           instanceVar, adapterInstanceClass, partialStartVar);
 
                         Label isNull = a.createLabel();
@@ -993,7 +972,7 @@ public class GenericEncodingStrategy<S extends Storable> {
                            propType.toClass() == BigDecimal.class)
                 {
                     loadPropertyValue(stashedProperties, stashedFromInstances,
-                                      a, info, i, options,
+                                      a, info, i, useReadMethods,
                                       instanceVar, adapterInstanceClass, partialStartVar);
 
                     String methodName;
@@ -1179,7 +1158,7 @@ public class GenericEncodingStrategy<S extends Storable> {
 
             boolean fromInstance = loadPropertyValue
                 (stashedProperties, stashedFromInstances,
-                 a, info, i, options,
+                 a, info, i, useReadMethods,
                  instanceVar, adapterInstanceClass, partialStartVar);
 
             TypeDesc propType = info.getStorageType();
@@ -1279,7 +1258,8 @@ public class GenericEncodingStrategy<S extends Storable> {
      * @param info info for property to load
      * @param ordinal zero-based property ordinal, used only if instanceVar
      * refers to an object array.
-     * @param options optional encoding options
+     * @param useReadMethod when true, access property by public read method
+     * instead of protected field
      * @param instanceVar local variable referencing Storable instance,
      * defaults to "this" if null. If variable type is an Object array, then
      * property values are read from the runtime value of this array instead
@@ -1297,7 +1277,7 @@ public class GenericEncodingStrategy<S extends Storable> {
                                         Boolean[] stashedFromInstances,
                                         CodeAssembler a,
                                         StorablePropertyInfo info, int ordinal,
-                                        EnumSet<Option> options,
+                                        boolean useReadMethod,
                                         LocalVariable instanceVar,
                                         Class<?> adapterInstanceClass,
                                         LocalVariable partialStartVar)
@@ -1308,7 +1288,7 @@ public class GenericEncodingStrategy<S extends Storable> {
         }
 
         boolean fromInstance = loadPropertyValue
-            (a, info, ordinal, options, instanceVar, adapterInstanceClass, partialStartVar);
+            (a, info, ordinal, useReadMethod, instanceVar, adapterInstanceClass, partialStartVar);
 
         if (stashedProperties != null) {
             LocalVariable propVar = stashedProperties[ordinal];
@@ -1329,7 +1309,8 @@ public class GenericEncodingStrategy<S extends Storable> {
      * @param info info for property to load
      * @param ordinal zero-based property ordinal, used only if instanceVar
      * refers to an object array.
-     * @param options optional encoding options
+     * @param useReadMethod when true, access property by public read method
+     * instead of protected field
      * @param instanceVar local variable referencing Storable instance,
      * defaults to "this" if null. If variable type is an Object array, then
      * property values are read from the runtime value of this array instead
@@ -1345,7 +1326,7 @@ public class GenericEncodingStrategy<S extends Storable> {
      */
     protected boolean loadPropertyValue(CodeAssembler a,
                                         StorablePropertyInfo info, int ordinal,
-                                        EnumSet<Option> options,
+                                        boolean useReadMethod,
                                         LocalVariable instanceVar,
                                         Class<?> adapterInstanceClass,
                                         LocalVariable partialStartVar)
@@ -1356,11 +1337,9 @@ public class GenericEncodingStrategy<S extends Storable> {
         final boolean isObjectArrayInstanceVar = instanceVar != null
             && instanceVar.getType() == TypeDesc.forClass(Object[].class);
 
-        final boolean useMethod = options != null && options.contains(Option.USE_METHODS);
-
         final boolean useAdapterInstance = adapterInstanceClass != null
             && info.getToStorageAdapter() != null
-            && (useMethod || isObjectArrayInstanceVar);
+            && (useReadMethod || isObjectArrayInstanceVar);
 
         if (useAdapterInstance) {
             // Push adapter instance to stack to be used later.
@@ -1373,7 +1352,7 @@ public class GenericEncodingStrategy<S extends Storable> {
 
         if (instanceVar == null) {
             a.loadThis();
-            if (useMethod) {
+            if (useReadMethod) {
                 info.addInvokeReadMethod(a);
             } else {
                 // Access property value directly from protected field of "this".
@@ -1386,7 +1365,7 @@ public class GenericEncodingStrategy<S extends Storable> {
             }
         } else if (!isObjectArrayInstanceVar) {
             a.loadLocal(instanceVar);
-            if (useMethod) {
+            if (useReadMethod) {
                 info.addInvokeReadMethod(a, instanceVar.getType());
             } else {
                 // Access property value directly from protected field of
@@ -1420,18 +1399,6 @@ public class GenericEncodingStrategy<S extends Storable> {
         if (useAdapterInstance) {
             // Invoke adapter method on instance pushed earlier.
             a.invoke(info.getToStorageAdapter());
-        } else {
-            if (options != null && options.contains(Option.NORMALIZE)) {
-                TypeDesc bdType = TypeDesc.forClass(BigDecimal.class);
-                if (type == bdType) {
-                    // Normalize by stripping trailing zeros.
-                    a.dup();
-                    Label isNull = a.createLabel();
-                    a.ifNullBranch(isNull, true);
-                    a.invokeVirtual(bdType, "stripTrailingZeros", bdType, null);
-                    isNull.setLocation();
-                }
-            }
         }
 
         return !isObjectArrayInstanceVar;
@@ -1831,7 +1798,7 @@ public class GenericEncodingStrategy<S extends Storable> {
                                Direction[] directions,
                                LocalVariable instanceVar,
                                Class<?> adapterInstanceClass,
-                               EnumSet<Option> options,
+                               boolean useWriteMethods,
                                int generation,
                                Label altGenerationHandler,
                                LocalVariable encodedVar)
@@ -1936,7 +1903,7 @@ public class GenericEncodingStrategy<S extends Storable> {
                     // Just store raw property value.
                 }
 
-                storePropertyValue(a, info, options, instanceVar, adapterInstanceClass);
+                storePropertyValue(a, info, useWriteMethods, instanceVar, adapterInstanceClass);
                 return;
             }
         }
@@ -2150,7 +2117,7 @@ public class GenericEncodingStrategy<S extends Storable> {
 
             storePropertyLocation.setLocation();
 
-            storePropertyValue(a, info, options, instanceVar, adapterInstanceClass);
+            storePropertyValue(a, info, useWriteMethods, instanceVar, adapterInstanceClass);
 
             nextPropertyLocation.setLocation();
         }
@@ -2370,7 +2337,8 @@ public class GenericEncodingStrategy<S extends Storable> {
      * array must also be on the operand stack.
      *
      * @param info info for property to store to
-     * @param options optional encoding options
+     * @param useWriteMethod when true, set property by public write method
+     * instead of protected field
      * @param instanceVar local variable referencing Storable instance,
      * defaults to "this" if null. If variable type is an Object array, then
      * property values are written to the runtime value of this array instead
@@ -2380,7 +2348,7 @@ public class GenericEncodingStrategy<S extends Storable> {
      * @see #pushDecodingInstanceVar pushDecodingInstanceVar
      */
     protected void storePropertyValue(CodeAssembler a, StorablePropertyInfo info,
-                                      EnumSet<Option> options,
+                                      boolean useWriteMethod,
                                       LocalVariable instanceVar,
                                       Class<?> adapterInstanceClass) {
         TypeDesc type = info.getPropertyType();
@@ -2389,11 +2357,9 @@ public class GenericEncodingStrategy<S extends Storable> {
         boolean isObjectArrayInstanceVar = instanceVar != null
             && instanceVar.getType() == TypeDesc.forClass(Object[].class);
 
-        final boolean useMethod = options != null && options.contains(Option.USE_METHODS);
-
         boolean useAdapterInstance = adapterInstanceClass != null
             && info.getToStorageAdapter() != null
-            && (useMethod || isObjectArrayInstanceVar);
+            && (useWriteMethod || isObjectArrayInstanceVar);
 
         if (useAdapterInstance) {
             // Push adapter instance to adapt property value. It must be on the
@@ -2416,7 +2382,7 @@ public class GenericEncodingStrategy<S extends Storable> {
         }
 
         if (instanceVar == null) {
-            if (useMethod) {
+            if (useWriteMethod) {
                 info.addInvokeWriteMethod(a);
             } else {
                 // Set property value directly to protected field of instance.
@@ -2499,7 +2465,7 @@ public class GenericEncodingStrategy<S extends Storable> {
                 return;
             }
 
-            if (useMethod) {
+            if (useWriteMethod) {
                 info.addInvokeWriteMethod(a, instanceVarType);
             } else {
                 // Set property value directly to protected field of referenced
diff --git a/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java b/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java
index 3a642e4..dd43515 100644
--- a/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java
+++ b/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java
@@ -21,7 +21,6 @@ package com.amazon.carbonado.raw;
 import java.lang.ref.WeakReference;
 import java.lang.reflect.Method;
 import java.lang.reflect.UndeclaredThrowableException;
-import java.util.EnumSet;
 import java.util.Map;
 
 import org.cojen.classfile.ClassFile;
@@ -56,8 +55,6 @@ import com.amazon.carbonado.gen.CodeBuilderUtil;
 import com.amazon.carbonado.util.ThrowUnchecked;
 import com.amazon.carbonado.util.QuickConstructorGenerator;
 
-import static com.amazon.carbonado.raw.GenericEncodingStrategy.Option;
-
 /**
  * Generic codec that supports any kind of storable by auto-generating and
  * caching storable implementations.
@@ -200,17 +197,15 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
 
             // TODO: Consider caching generated key. Rebuild if null or if pk is dirty.
 
-            EnumSet<Option> options = EnumSet.of(Option.NORMALIZE);
-
             // assembler            = b
             // properties           = null (defaults to all key properties)
             // instanceVar          = null (null means "this")
             // adapterInstanceClass = null (null means use instanceVar, in this case is "this")
-            // options              = options
+            // useReadMethods       = false (will read fields directly)
             // partialStartVar      = null (only support encoding all properties)
             // partialEndVar        = null (only support encoding all properties)
             LocalVariable encodedVar =
-                encodingStrategy.buildKeyEncoding(b, null, null, null, options, null, null);
+                encodingStrategy.buildKeyEncoding(b, null, null, null, false, null, null);
 
             b.loadLocal(encodedVar);
             b.returnValue(byteArrayType);
@@ -224,16 +219,14 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
                                          byteArrayType, null);
             CodeBuilder b = new CodeBuilder(mi);
 
-            EnumSet<Option> options = EnumSet.of(Option.NORMALIZE);
-
             // assembler            = b
             // properties           = null (defaults to all non-key properties)
             // instanceVar          = null (null means "this")
             // adapterInstanceClass = null (null means use instanceVar, in this case is "this")
-            // options              = options
+            // useReadMethods       = false (will read fields directly)
             // generation           = generation
             LocalVariable encodedVar =
-                encodingStrategy.buildDataEncoding(b, null, null, null, options, generation);
+                encodingStrategy.buildDataEncoding(b, null, null, null, false, generation);
 
             b.loadLocal(encodedVar);
             b.returnValue(byteArrayType);
@@ -250,9 +243,9 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
             // properties           = null (defaults to all key properties)
             // instanceVar          = null (null means "this")
             // adapterInstanceClass = null (null means use instanceVar, in this case is "this")
-            // options              = null (will set fields directly)
+            // useWriteMethods      = false (will set fields directly)
             // encodedVar           = references byte array with encoded key
-            encodingStrategy.buildKeyDecoding(b, null, null, null, null, b.getParameter(0));
+            encodingStrategy.buildKeyDecoding(b, null, null, null, false, b.getParameter(0));
 
             b.returnVoid();
         }
@@ -269,12 +262,12 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
             // properties           = null (defaults to all non-key properties)
             // instanceVar          = null (null means "this")
             // adapterInstanceClass = null (null means use instanceVar, in this case is "this")
-            // options              = null (will set fields directly)
+            // useWriteMethods      = false (will set fields directly)
             // generation           = generation
             // altGenerationHandler = altGenerationHandler
             // encodedVar           = references byte array with encoded data
             encodingStrategy.buildDataDecoding
-                (b, null, null, null, null, generation, altGenerationHandler, b.getParameter(0));
+                (b, null, null, null, false, generation, altGenerationHandler, b.getParameter(0));
 
             b.returnVoid();
 
@@ -591,17 +584,15 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
             LocalVariable instanceVar = b.createLocalVariable(null, instanceType);
             b.storeLocal(instanceVar);
 
-            EnumSet<Option> options = EnumSet.of(Option.NORMALIZE);
-
             // assembler            = b
             // properties           = properties to encode
             // instanceVar          = instanceVar which references storable instance
             // adapterInstanceClass = null (null means use instanceVar)
-            // options              = options
+            // useReadMethods       = false (will read fields directly)
             // partialStartVar      = null (only support encoding all properties)
             // partialEndVar        = null (only support encoding all properties)
             LocalVariable encodedVar = mEncodingStrategy.buildKeyEncoding
-                (b, properties, instanceVar, null, options, null, null);
+                (b, properties, instanceVar, null, false, null, null);
 
             b.loadLocal(encodedVar);
             b.returnValue(byteArrayType);
@@ -622,17 +613,15 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
             LocalVariable instanceVar = b.createLocalVariable(null, instanceType);
             b.storeLocal(instanceVar);
 
-            EnumSet<Option> options = EnumSet.of(Option.NORMALIZE);
-
             // assembler            = b
             // properties           = properties to encode
             // instanceVar          = instanceVar which references storable instance
             // adapterInstanceClass = null (null means use instanceVar)
-            // options              = options
+            // useReadMethods       = false (will read fields directly)
             // partialStartVar      = int parameter 1, references start property index
             // partialEndVar        = int parameter 2, references end property index
             LocalVariable encodedVar = mEncodingStrategy.buildKeyEncoding
-                (b, properties, instanceVar, null, options, b.getParameter(1), b.getParameter(2));
+                (b, properties, instanceVar, null, false, b.getParameter(1), b.getParameter(2));
 
             b.loadLocal(encodedVar);
             b.returnValue(byteArrayType);
@@ -656,17 +645,15 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
                  new TypeDesc[] {objectArrayType});
             CodeBuilder b = new CodeBuilder(mi);
 
-            EnumSet<Option> options = EnumSet.of(Option.NORMALIZE);
-
             // assembler            = b
             // properties           = properties to encode
             // instanceVar          = parameter 0, an object array
             // adapterInstanceClass = adapterInstanceClass - see comment above
-            // options              = options
+            // useReadMethods       = false (will read fields directly)
             // partialStartVar      = null (only support encoding all properties)
             // partialEndVar        = null (only support encoding all properties)
             LocalVariable encodedVar = mEncodingStrategy.buildKeyEncoding
-                (b, properties, b.getParameter(0), adapterInstanceClass, options, null, null);
+                (b, properties, b.getParameter(0), adapterInstanceClass, false, null, null);
 
             b.loadLocal(encodedVar);
             b.returnValue(byteArrayType);
@@ -683,18 +670,16 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
                  new TypeDesc[] {objectArrayType, TypeDesc.INT, TypeDesc.INT});
             CodeBuilder b = new CodeBuilder(mi);
 
-            EnumSet<Option> options = EnumSet.of(Option.NORMALIZE);
-
             // assembler            = b
             // properties           = properties to encode
             // instanceVar          = parameter 0, an object array
             // adapterInstanceClass = adapterInstanceClass - see comment above
-            // options              = options
+            // useReadMethods       = false (will read fields directly)
             // partialStartVar      = int parameter 1, references start property index
             // partialEndVar        = int parameter 2, references end property index
             LocalVariable encodedVar = mEncodingStrategy.buildKeyEncoding
                 (b, properties, b.getParameter(0), adapterInstanceClass,
-                 options, b.getParameter(1), b.getParameter(2));
+                 false, b.getParameter(1), b.getParameter(2));
 
             b.loadLocal(encodedVar);
             b.returnValue(byteArrayType);
@@ -730,11 +715,11 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
                 // properties           = no parameters - we just want the key prefix
                 // instanceVar          = null (no parameters means we don't need this)
                 // adapterInstanceClass = null (no parameters means we don't need this)
-                // options              = null (no parameters means we don't need this)
+                // useReadMethods       = false (no parameters means we don't need this)
                 // partialStartVar      = null (no parameters means we don't need this)
                 // partialEndVar        = null (no parameters means we don't need this)
                 LocalVariable encodedVar = mEncodingStrategy.buildKeyEncoding
-                    (b, new OrderedProperty[0], null, null, null, null, null);
+                    (b, new OrderedProperty[0], null, null, false, null, null);
 
                 b.loadLocal(encodedVar);
                 b.storeStaticField(BLANK_KEY_FIELD_NAME, byteArrayType);
@@ -798,13 +783,13 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S
         // properties           = null (defaults to all non-key properties)
         // instanceVar          = "dest" storable
         // adapterInstanceClass = null (null means use instanceVar, in this case is "dest")
-        // options              = null (will set fields directly)
+        // useWriteMethods      = false (will set fields directly)
         // generation           = generation
         // altGenerationHandler = null (generation should match)
         // encodedVar           = "data" byte array
         try {
             altStrategy.buildDataDecoding
-                (b, null, destVar, null, null, generation, null, dataVar);
+                (b, null, destVar, null, false, generation, null, dataVar);
         } catch (SupportException e) {
             throw new CorruptEncodingException(e);
         }
diff --git a/src/main/java/com/amazon/carbonado/raw/RawStorableGenerator.java b/src/main/java/com/amazon/carbonado/raw/RawStorableGenerator.java
index 57467cc..07f38a7 100644
--- a/src/main/java/com/amazon/carbonado/raw/RawStorableGenerator.java
+++ b/src/main/java/com/amazon/carbonado/raw/RawStorableGenerator.java
@@ -180,11 +180,13 @@ public class RawStorableGenerator {
         EnumSet<MasterFeature> features;
         if (isMaster) {
             features = EnumSet.of(MasterFeature.VERSIONING,
+                                  MasterFeature.NORMALIZE,
                                   MasterFeature.UPDATE_FULL,
                                   MasterFeature.INSERT_SEQUENCES,
                                   MasterFeature.INSERT_CHECK_REQUIRED);
         } else {
-            features = EnumSet.of(MasterFeature.UPDATE_FULL);
+            features = EnumSet.of(MasterFeature.NORMALIZE,
+                                  MasterFeature.UPDATE_FULL);
         }
 
         final Class<? extends S> abstractClass =
diff --git a/src/main/java/com/amazon/carbonado/repo/map/MapStorage.java b/src/main/java/com/amazon/carbonado/repo/map/MapStorage.java
index 7c2fa0a..27934ef 100644
--- a/src/main/java/com/amazon/carbonado/repo/map/MapStorage.java
+++ b/src/main/java/com/amazon/carbonado/repo/map/MapStorage.java
@@ -132,10 +132,12 @@ class MapStorage<S extends Storable>
         EnumSet<MasterFeature> features;
         if (repo.isMaster()) {
             features = EnumSet.of(MasterFeature.INSERT_CHECK_REQUIRED,
+                                  MasterFeature.NORMALIZE,
                                   MasterFeature.VERSIONING,
                                   MasterFeature.INSERT_SEQUENCES);
         } else {
-            features = EnumSet.of(MasterFeature.INSERT_CHECK_REQUIRED);
+            features = EnumSet.of(MasterFeature.INSERT_CHECK_REQUIRED,
+                                  MasterFeature.NORMALIZE);
         }
 
         Class<? extends S> delegateStorableClass =
diff --git a/src/main/java/com/amazon/carbonado/util/ConversionComparator.java b/src/main/java/com/amazon/carbonado/util/ConversionComparator.java
new file mode 100644
index 0000000..71f52a3
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/util/ConversionComparator.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates.  All rights reserved.
+ *
+ * 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.amazon.carbonado.util;
+
+import java.util.Comparator;
+
+import org.cojen.classfile.TypeDesc;
+
+/**
+ * Compares type conversions, finding the one that is nearest.
+ *
+ * @author Brian S O'Neill
+ * @since 1.2
+ */
+public class ConversionComparator implements Comparator<Class> {
+    private final TypeDesc mFrom;
+
+    public ConversionComparator(Class fromType) {
+        mFrom = TypeDesc.forClass(fromType);
+    }
+
+    /**
+     * Returns true if a coversion is possible to the given type.
+     */
+    public boolean isConversionPossible(Class toType) {
+        return isConversionPossible(mFrom, TypeDesc.forClass(toType));
+    }
+
+    @SuppressWarnings("unchecked")
+    private static boolean isConversionPossible(TypeDesc from, TypeDesc to) {
+        if (from == to) {
+            return true;
+        }
+
+        if (from.toPrimitiveType() != null && to.toPrimitiveType() != null) {
+            from = from.toPrimitiveType();
+            to = to.toPrimitiveType();
+        } else {
+            from = from.toObjectType();
+            to = to.toObjectType();
+        }
+
+        switch (from.getTypeCode()) {
+        case TypeDesc.OBJECT_CODE: default:
+            return to.toClass().isAssignableFrom(from.toClass());
+        case TypeDesc.BOOLEAN_CODE:
+            return to == TypeDesc.BOOLEAN;
+        case TypeDesc.BYTE_CODE:
+            return to == TypeDesc.BYTE || to == TypeDesc.SHORT
+                || to == TypeDesc.INT || to == TypeDesc.LONG
+                || to == TypeDesc.FLOAT || to == TypeDesc.DOUBLE;
+        case TypeDesc.SHORT_CODE:
+            return to == TypeDesc.SHORT
+                || to == TypeDesc.INT || to == TypeDesc.LONG
+                || to == TypeDesc.FLOAT || to == TypeDesc.DOUBLE;
+        case TypeDesc.CHAR_CODE:
+            return to == TypeDesc.CHAR;
+        case TypeDesc.INT_CODE:
+            return to == TypeDesc.INT || to == TypeDesc.LONG || to == TypeDesc.DOUBLE;
+        case TypeDesc.FLOAT_CODE:
+            return to == TypeDesc.FLOAT || to == TypeDesc.DOUBLE;
+        case TypeDesc.LONG_CODE:
+            return to == TypeDesc.LONG;
+        case TypeDesc.DOUBLE_CODE:
+            return to == TypeDesc.DOUBLE;
+        }
+    }
+
+    /**
+     * Evaluates two types, to see which one is nearest to the from type.
+     * Return {@literal <0} if "a" is nearest, 0 if both are equally good,
+     * {@literal >0} if "b" is nearest.
+     */
+    public int compare(Class toType_a, Class toType_b) {
+        TypeDesc from = mFrom;
+        TypeDesc a = TypeDesc.forClass(toType_a);
+        TypeDesc b = TypeDesc.forClass(toType_b);
+
+        if (from == a) {
+            if (from == b) {
+                return 0;
+            }
+            return -1;
+        } else if (from == b) {
+            return 1;
+        }
+
+        int result = compare(from, a, b);
+        if (result != 0) {
+            return result;
+        }
+
+        if (from.isPrimitive()) {
+            // Try boxing.
+            if (from.toObjectType() != null) {
+                from = from.toObjectType();
+                return compare(from, a, b);
+            }
+        } else {
+            // Try unboxing.
+            if (from.toPrimitiveType() != null) {
+                from = from.toPrimitiveType();
+                result = compare(from, a, b);
+                if (result != 0) {
+                    return result;
+                }
+                // Try boxing back up. Test by unboxing 'to' types.
+                if (!toType_a.isPrimitive() && a.toPrimitiveType() != null) {
+                    a = a.toPrimitiveType();
+                }
+                if (!toType_b.isPrimitive() && b.toPrimitiveType() != null) {
+                    b = b.toPrimitiveType();
+                }
+                return compare(from, a, b);
+            }
+        }
+
+        return 0;
+    }
+
+    private static int compare(TypeDesc from, TypeDesc a, TypeDesc b) {
+        if (isConversionPossible(from, a)) {
+            if (isConversionPossible(from, b)) {
+                if (from.isPrimitive()) {
+                    if (a.isPrimitive()) {
+                        if (b.isPrimitive()) {
+                            // Choose the one with the least amount of widening.
+                            return primitiveWidth(a) - primitiveWidth(b);
+                        } else {
+                            return -1;
+                        }
+                    } else if (b.isPrimitive()) {
+                        return 1;
+                    }
+                } else {
+                    // Choose the one with the shortest distance up the class
+                    // hierarchy.
+                    Class fromClass = from.toClass();
+                    if (!fromClass.isInterface()) {
+                        if (a.toClass().isInterface()) {
+                            if (!b.toClass().isInterface()) {
+                                return -1;
+                            }
+                        } else if (b.toClass().isInterface()) {
+                            return 1;
+                        } else {
+                            return distance(fromClass, a.toClass())
+                                - distance(fromClass, b.toClass());
+                        }
+                    }
+                }
+            } else {
+                return -1;
+            }
+        } else if (isConversionPossible(from, b)) {
+            return 1;
+        }
+
+        return 0;
+    }
+
+    // 1 = boolean, 2 = byte, 3 = short, 4 = char, 5 = int, 6 = float, 7 = long, 8 = double
+    private static int primitiveWidth(TypeDesc type) {
+        switch (type.getTypeCode()) {
+        default:
+            return 0;
+        case TypeDesc.BOOLEAN_CODE:
+            return 1;
+        case TypeDesc.BYTE_CODE:
+            return 2;
+        case TypeDesc.SHORT_CODE:
+            return 3;
+        case TypeDesc.CHAR_CODE:
+            return 4;
+        case TypeDesc.INT_CODE:
+            return 5;
+        case TypeDesc.FLOAT_CODE:
+            return 6;
+        case TypeDesc.LONG_CODE:
+            return 7;
+        case TypeDesc.DOUBLE_CODE:
+            return 8;
+        }
+    }
+
+    private static int distance(Class from, Class to) {
+        int distance = 0;
+        while (from != to) {
+            from = from.getSuperclass();
+            if (from == null) {
+                return Integer.MAX_VALUE;
+            }
+            distance++;
+        }
+        return distance;
+    }
+}
diff --git a/src/main/java/com/amazon/carbonado/util/Converter.java b/src/main/java/com/amazon/carbonado/util/Converter.java
new file mode 100644
index 0000000..6f1b479
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/util/Converter.java
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2008 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates.  All rights reserved.
+ *
+ * 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.amazon.carbonado.util;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import java.math.BigInteger;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.cojen.classfile.ClassFile;
+import org.cojen.classfile.CodeBuilder;
+import org.cojen.classfile.Label;
+import org.cojen.classfile.LocalVariable;
+import org.cojen.classfile.MethodInfo;
+import org.cojen.classfile.Modifiers;
+import org.cojen.classfile.Opcode;
+import org.cojen.classfile.TypeDesc;
+
+import org.cojen.util.ClassInjector;
+import org.cojen.util.SoftValuedHashMap;
+
+/**
+ * General purpose type converter. Custom conversions are possible by supplying
+ * an abstract subclass which has public conversion methods whose names begin
+ * with "convert". Each conversion method takes a single argument and returns a
+ * value.
+ *
+ * @author Brian S O'Neill
+ * @since 1.2
+ */
+public abstract class Converter {
+    private static final Map<Class, Converter> cCache = new SoftValuedHashMap<Class, Converter>();
+
+    public static synchronized <C extends Converter> C build(Class<C> converterType) {
+        C converter = (C) cCache.get(converterType);
+        if (converter == null) {
+            converter = new Builder<C>(converterType).build();
+            cCache.put(converterType, converter);
+        }
+        return converter;
+    }
+
+    /**
+     * @throws IllegalArgumentException if conversion is not supported
+     */
+    public abstract <T> T convert(Object from, Class<T> toType);
+
+    /**
+     * @throws IllegalArgumentException if conversion is not supported
+     */
+    public abstract <T> T convert(byte from, Class<T> toType);
+
+    /**
+     * @throws IllegalArgumentException if conversion is not supported
+     */
+    public abstract <T> T convert(short from, Class<T> toType);
+
+    /**
+     * @throws IllegalArgumentException if conversion is not supported
+     */
+    public abstract <T> T convert(int from, Class<T> toType);
+
+    /**
+     * @throws IllegalArgumentException if conversion is not supported
+     */
+    public abstract <T> T convert(long from, Class<T> toType);
+
+    /**
+     * @throws IllegalArgumentException if conversion is not supported
+     */
+    public abstract <T> T convert(float from, Class<T> toType);
+
+    /**
+     * @throws IllegalArgumentException if conversion is not supported
+     */
+    public abstract <T> T convert(double from, Class<T> toType);
+
+    /**
+     * @throws IllegalArgumentException if conversion is not supported
+     */
+    public abstract <T> T convert(boolean from, Class<T> toType);
+
+    /**
+     * @throws IllegalArgumentException if conversion is not supported
+     */
+    public abstract <T> T convert(char from, Class<T> toType);
+
+    protected IllegalArgumentException conversionNotSupported
+        (Object fromValue, Class fromType, Class toType)
+    {
+        StringBuilder b = new StringBuilder();
+
+        if (fromType == null && fromValue != null) {
+            fromType = fromValue.getClass();
+        }
+
+        if (fromValue == null) {
+            b.append("Actual value null cannot be converted to type ");
+        } else {
+            b.append("Actual value \"");
+            b.append(String.valueOf(fromValue));
+            b.append("\", of type \"");
+            b.append(TypeDesc.forClass(fromType).getFullName());
+            b.append("\", cannot be converted to expected type of ");
+        }
+
+        if (toType == null) {
+            b.append("null");
+        } else {
+            b.append('"');
+            b.append(TypeDesc.forClass(toType).getFullName());
+            b.append('"');
+        }
+
+        return new IllegalArgumentException(b.toString());
+    }
+
+    private static class Builder<C extends Converter> {
+        private final Class<C> mConverterType;
+
+        // Map "from class" to "to class" to optional conversion method.
+        private final Map<Class, Map<Class, Method>> mConvertMap;
+
+        private final Class[][] mBoxMatrix = {
+            {byte.class, Byte.class, Number.class, Object.class},
+            {short.class, Short.class, Number.class, Object.class},
+            {int.class, Integer.class, Number.class, Object.class},
+            {long.class, Long.class, Number.class, Object.class},
+            {float.class, Float.class, Number.class, Object.class},
+            {double.class, Double.class, Number.class, Object.class},
+            {boolean.class, Boolean.class, Object.class},
+            {char.class, Character.class, Object.class},
+        };
+
+        private ClassFile mClassFile;
+
+        private int mInnerConvertCounter;
+
+        Builder(Class<C> converterType) {
+            if (!Converter.class.isAssignableFrom(converterType)) {
+                throw new IllegalArgumentException("Not a TypeConverter: " + converterType);
+            }
+
+            try {
+                converterType.getConstructor();
+            } catch (NoSuchMethodException e) {
+                throw new IllegalArgumentException
+                    ("TypeConverter must have a public no-arg constructor: " + converterType);
+            }
+
+            mConverterType = converterType;
+            mConvertMap = new HashMap<Class, Map<Class, Method>>();
+
+            // Add built-in primitive boxing/unboxing conversions.
+            for (Class[] tuple : mBoxMatrix) {
+                Map<Class, Method> to = new HashMap<Class, Method>();
+                for (Class toType : tuple) {
+                    to.put(toType, null);
+                }
+                mConvertMap.put(tuple[0], to);
+                mConvertMap.put(tuple[1], to);
+            }
+
+            for (Method m : converterType.getMethods()) {
+                if (!m.getName().startsWith("convert")) {
+                    continue;
+                }
+                Class toType = m.getReturnType();
+                if (toType == null || toType == void.class) {
+                    continue;
+                }
+                Class[] params = m.getParameterTypes();
+                if (params == null || params.length != 1) {
+                    continue;
+                }
+
+                Map<Class, Method> to = mConvertMap.get(params[0]);
+                if (to == null) {
+                    to = new HashMap<Class, Method>();
+                    mConvertMap.put(params[0], to);
+                }
+
+                to.put(toType, m);
+            }
+
+            // Add automatic widening conversions.
+
+            // Copy to prevent concurrent modification.
+            Map<Class, Map<Class, Method>> convertMap =
+                new HashMap<Class, Map<Class, Method>>(mConvertMap);
+
+            for (Map.Entry<Class, Map<Class, Method>> entry : convertMap.entrySet()) {
+                Class fromType = entry.getKey();
+
+                // Copy to prevent concurrent modification.
+                Map<Class, Method> toMap = new HashMap<Class, Method>(entry.getValue());
+
+                for (Map.Entry<Class, Method> to : toMap.entrySet()) {
+                    Class toType = to.getKey();
+                    Method conversionMethod = to.getValue();
+                    addAutomaticConversion(fromType, toType, conversionMethod);
+                }
+            }
+
+            /*
+            for (Map.Entry<Class, Map<Class, Method>> entry : mConvertMap.entrySet()) {
+                Class fromType = entry.getKey();
+                for (Map.Entry<Class, Method> to : entry.getValue().entrySet()) {
+                    Class toType = to.getKey();
+                    Method conversionMethod = to.getValue();
+                    System.out.println("from: " + fromType.getName() + ", to: " +
+                                       toType.getName() + ", via: " + conversionMethod);
+                }
+            }
+            */
+        }
+
+        C build() {
+            ClassInjector ci = ClassInjector
+                .create(mConverterType.getName(), mConverterType.getClassLoader());
+
+            mClassFile = new ClassFile(ci.getClassName(), mConverterType);
+            mClassFile.markSynthetic();
+            mClassFile.setSourceFile(Converter.class.getName());
+            mClassFile.setTarget("1.5");
+
+            mClassFile.addDefaultConstructor();
+
+            addPrimitiveConvertMethod(byte.class);
+            addPrimitiveConvertMethod(short.class);
+            addPrimitiveConvertMethod(int.class);
+            addPrimitiveConvertMethod(long.class);
+            addPrimitiveConvertMethod(float.class);
+            addPrimitiveConvertMethod(double.class);
+            addPrimitiveConvertMethod(boolean.class);
+            addPrimitiveConvertMethod(char.class);
+
+            Method m = getAbstractConvertMethod(Object.class);
+            if (m != null) {
+                CodeBuilder b = new CodeBuilder(mClassFile.addMethod(m));
+
+                b.loadLocal(b.getParameter(0));
+                Label notNull = b.createLabel();
+                b.ifNullBranch(notNull, false);
+                b.loadNull();
+                b.returnValue(TypeDesc.OBJECT);
+
+                notNull.setLocation();
+                addConversionSwitch(b, null);
+            }
+
+            Class clazz = ci.defineClass(mClassFile);
+
+            try {
+                return (C) clazz.newInstance();
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException(e);
+            } catch (InstantiationException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        private void addPrimitiveConvertMethod(Class fromType) {
+            Method m = getAbstractConvertMethod(fromType);
+            if (m == null) {
+                return;
+            }
+
+            CodeBuilder b = new CodeBuilder(mClassFile.addMethod(m));
+
+            addConversionSwitch(b, fromType);
+        }
+
+        /*
+         * Generate big switch statements that operate on Classes.
+         *
+         * For switch case count, obtain a prime number, at least twice as
+         * large as needed. This should minimize hash collisions. Since all
+         * the hash keys are known up front, the capacity could be tweaked
+         * until there are no collisions, but this technique is easier and
+         * deterministic.
+         */
+
+        private void addConversionSwitch(CodeBuilder b, Class fromType) {
+            Map<Class, Method> toMap;
+            Map<Class, ?> caseMap;
+
+            if (fromType == null) {
+                Map<Class, Map<Class, Method>> convertMap =
+                    new HashMap<Class, Map<Class, Method>>(mConvertMap);
+                // Remove primitive type cases, since they will never match.
+                Iterator<Class> it = convertMap.keySet().iterator();
+                while (it.hasNext()) {
+                    if (it.next().isPrimitive()) {
+                        it.remove();
+                    }
+                }
+
+                toMap = null;
+                caseMap = convertMap;
+            } else {
+                toMap = mConvertMap.get(fromType);
+                caseMap = toMap;
+            }
+
+            int caseCount = caseCount(caseMap.size());
+
+            int[] cases = new int[caseCount];
+            for (int i=0; i<caseCount; i++) {
+                cases[i] = i;
+            }
+
+            Label[] switchLabels = new Label[caseCount];
+            Label noMatch = b.createLabel();
+            List<Class>[] caseMatches = caseMatches(caseMap, caseCount);
+
+            for (int i=0; i<caseCount; i++) {
+                List<?> matches = caseMatches[i];
+                if (matches == null || matches.size() == 0) {
+                    switchLabels[i] = noMatch;
+                } else {
+                    switchLabels[i] = b.createLabel();
+                }
+            }
+
+            final TypeDesc classType = TypeDesc.forClass(Class.class);
+
+            LocalVariable caseVar;
+            if (toMap == null) {
+                b.loadLocal(b.getParameter(0));
+                b.invokeVirtual(TypeDesc.OBJECT, "getClass", classType, null);
+                caseVar = b.createLocalVariable(null, classType);
+                b.storeLocal(caseVar);
+            } else {
+                caseVar = b.getParameter(1);
+            }
+
+            if (caseMap.size() > 1) {
+                b.loadLocal(caseVar);
+
+                b.invokeVirtual(Class.class.getName(), "hashCode", TypeDesc.INT, null);
+                b.loadConstant(0x7fffffff);
+                b.math(Opcode.IAND);
+                b.loadConstant(caseCount);
+                b.math(Opcode.IREM);
+
+                b.switchBranch(cases, switchLabels, noMatch);
+            }
+
+            TypeDesc fromTypeDesc = TypeDesc.forClass(fromType);
+
+            for (int i=0; i<caseCount; i++) {
+                List<Class> matches = caseMatches[i];
+                if (matches == null || matches.size() == 0) {
+                    continue;
+                }
+
+                switchLabels[i].setLocation();
+
+                int matchCount = matches.size();
+                for (int j=0; j<matchCount; j++) {
+                    Class toType = matches.get(j);
+                    TypeDesc toTypeDesc = TypeDesc.forClass(toType);
+
+                    // Test against class instance to find exact match.
+
+                    b.loadConstant(toTypeDesc);
+                    b.loadLocal(caseVar);
+                    Label notEqual;
+                    if (j == matchCount - 1) {
+                        notEqual = null;
+                        b.ifEqualBranch(noMatch, false);
+                    } else {
+                        notEqual = b.createLabel();
+                        b.ifEqualBranch(notEqual, false);
+                    }
+
+                    if (toMap == null) {
+                        // Switch in a switch, but do so in a separate method
+                        // to keep this one small.
+
+                        String name = "convert$" + (++mInnerConvertCounter);
+                        TypeDesc[] params = {toTypeDesc, classType};
+                        {
+                            MethodInfo mi = mClassFile.addMethod
+                                (Modifiers.PRIVATE, name, TypeDesc.OBJECT, params);
+                            CodeBuilder b2 = new CodeBuilder(mi);
+                            addConversionSwitch(b2, toType);
+                        }
+
+                        b.loadThis();
+                        b.loadLocal(b.getParameter(0));
+                        b.checkCast(toTypeDesc);
+                        b.loadLocal(b.getParameter(1));
+                        b.invokePrivate(name, TypeDesc.OBJECT, params);
+                        b.returnValue(TypeDesc.OBJECT);
+                    } else {
+                        Method convertMethod = toMap.get(toType);
+
+                        if (convertMethod == null) {
+                            b.loadLocal(b.getParameter(0));
+                            TypeDesc fromPrimDesc = fromTypeDesc.toPrimitiveType();
+                            if (fromPrimDesc != null) {
+                                b.convert(fromTypeDesc, fromPrimDesc);
+                                b.convert(fromPrimDesc, toTypeDesc.toObjectType());
+                            } else {
+                                b.convert(fromTypeDesc, toTypeDesc.toObjectType());
+                            }
+                        } else {
+                            b.loadThis();
+                            b.loadLocal(b.getParameter(0));
+                            Class paramType = convertMethod.getParameterTypes()[0];
+                            b.convert(fromTypeDesc, TypeDesc.forClass(paramType));
+                            b.invoke(convertMethod);
+                            TypeDesc retType = TypeDesc.forClass(convertMethod.getReturnType());
+                            b.convert(retType, retType.toObjectType());
+                        }
+
+                        b.returnValue(TypeDesc.OBJECT);
+                    }
+
+                    if (notEqual != null) {
+                        notEqual.setLocation();
+                    }
+                }
+            }
+
+            noMatch.setLocation();
+
+            final TypeDesc valueType = b.getParameter(0).getType();
+
+            if (fromType == null) {
+                // Check if object is already the desired type.
+
+                b.loadLocal(b.getParameter(1));
+                b.loadLocal(b.getParameter(0));
+                b.invokeVirtual(classType, "isInstance", TypeDesc.BOOLEAN,
+                                new TypeDesc[] {TypeDesc.OBJECT});
+                Label notSupported = b.createLabel();
+                b.ifZeroComparisonBranch(notSupported, "==");
+                b.loadLocal(b.getParameter(0));
+                b.convert(valueType, valueType.toObjectType());
+                b.returnValue(TypeDesc.OBJECT);
+
+                notSupported.setLocation();
+            }
+
+            b.loadThis();
+            b.loadLocal(b.getParameter(0));
+            b.convert(valueType, valueType.toObjectType());
+            if (valueType.isPrimitive()) {
+                b.loadConstant(valueType);
+            } else {
+                b.loadNull();
+            }
+            b.loadLocal(b.getParameter(1));
+            b.invokeVirtual("conversionNotSupported",
+                            TypeDesc.forClass(IllegalArgumentException.class),
+                            new TypeDesc[] {TypeDesc.OBJECT, classType, classType});
+            b.throwObject();
+        }
+
+        private int caseCount(int size) {
+            BigInteger capacity = BigInteger.valueOf(size * 2 + 1);
+            while (!capacity.isProbablePrime(100)) {
+                capacity = capacity.add(BigInteger.valueOf(2));
+            }
+            return capacity.intValue();
+        }
+
+        /**
+         * Returns the types that match on a given case. The array length is
+         * the same as the case count. Each list represents the matches. The
+         * lists themselves may be null if no matches for that case.
+         */
+        private List<Class>[] caseMatches(Map<Class, ?> caseMap, int caseCount) {
+            List<Class>[] cases = new List[caseCount];
+
+            for (Class to : caseMap.keySet()) {
+                int hashCode = to.hashCode();
+                int caseValue = (hashCode & 0x7fffffff) % caseCount;
+                List matches = cases[caseValue];
+                if (matches == null) {
+                    matches = cases[caseValue] = new ArrayList<Class>();
+                }
+                matches.add(to);
+            }
+
+            return cases;
+        }
+
+        /**
+         * @return null if should not be defined
+         */
+        private Method getAbstractConvertMethod(Class fromType) {
+            Method m;
+            try {
+                m = mConverterType.getMethod("convert", fromType, Class.class);
+            } catch (NoSuchMethodException e) {
+                return null;
+            }
+            if (!Modifier.isAbstract(m.getModifiers())) {
+                return null;
+            }
+            return m;
+        }
+
+        private void addAutomaticConversion(Class fromType, Class toType, Method method) {
+            addConversionIfNotExists(fromType, toType, method);
+
+            // Add no-op conversions.
+            addConversionIfNotExists(fromType, fromType, null);
+            addConversionIfNotExists(toType, toType, null);
+
+            for (Class[] pair : mBoxMatrix) {
+                if (fromType == pair[0]) {
+                    addConversionIfNotExists(pair[1], toType, method);
+                    if (toType == pair[1]) {
+                        addConversionIfNotExists(pair[1], pair[0], method);
+                    }
+                } else if (fromType == pair[1]) {
+                    addConversionIfNotExists(pair[0], toType, method);
+                    if (toType == pair[1]) {
+                        addConversionIfNotExists(pair[0], pair[1], method);
+                    }
+                }
+                if (toType == pair[0]) {
+                    addConversionIfNotExists(fromType, pair[1], method);
+                }
+            }
+
+            if (fromType == short.class || fromType == Short.class) {
+                addAutomaticConversion(byte.class, toType, method);
+            } else if (fromType == int.class || fromType == Integer.class) {
+                addAutomaticConversion(short.class, toType, method);
+            } else if (fromType == long.class || fromType == Long.class) {
+                addAutomaticConversion(int.class, toType, method);
+            } else if (fromType == float.class || fromType == Float.class) {
+                addAutomaticConversion(short.class, toType, method);
+            } else if (fromType == double.class || fromType == Double.class) {
+                addAutomaticConversion(int.class, toType, method);
+                addAutomaticConversion(float.class, toType, method);
+            }
+
+            if (toType == byte.class || toType == Byte.class) {
+                addAutomaticConversion(fromType, Short.class, method);
+            } else if (toType == short.class || toType == Short.class) {
+                addAutomaticConversion(fromType, Integer.class, method);
+                addAutomaticConversion(fromType, Float.class, method);
+            } else if (toType == int.class || toType == Integer.class) {
+                addAutomaticConversion(fromType, Long.class, method);
+                addAutomaticConversion(fromType, Double.class, method);
+            } else if (toType == float.class || toType == Float.class) {
+                addAutomaticConversion(fromType, Double.class, method);
+            }
+        }
+
+        private boolean addConversionIfNotExists(Class fromType, Class toType, Method method) {
+            Map<Class, Method> to = mConvertMap.get(fromType);
+            if (to == null) {
+                to = new HashMap<Class, Method>();
+                mConvertMap.put(fromType, to);
+            }
+            Method existing = to.get(toType);
+            if (existing != null) {
+                if (method == null) {
+                    return false;
+                }
+                ConversionComparator cc = new ConversionComparator(fromType);
+                Class existingFromType = existing.getParameterTypes()[0];
+                Class candidateFromType = method.getParameterTypes()[0];
+                if (cc.compare(existingFromType, candidateFromType) <= 0) {
+                    return false;
+                }
+            }
+            to.put(toType, method);
+            return true;
+        }
+    }
+}
-- 
cgit v1.2.3