From fec4f593ccac17493f0014bd8fabdedc278135c8 Mon Sep 17 00:00:00 2001 From: "Brian S. O'Neill" 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. --- .../carbonado/util/ConversionComparator.java | 213 ++++++++ .../java/com/amazon/carbonado/util/Converter.java | 602 +++++++++++++++++++++ 2 files changed, 815 insertions(+) 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/carbonado/util') 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 { + 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 cCache = new SoftValuedHashMap(); + + public static synchronized C build(Class converterType) { + C converter = (C) cCache.get(converterType); + if (converter == null) { + converter = new Builder(converterType).build(); + cCache.put(converterType, converter); + } + return converter; + } + + /** + * @throws IllegalArgumentException if conversion is not supported + */ + public abstract T convert(Object from, Class toType); + + /** + * @throws IllegalArgumentException if conversion is not supported + */ + public abstract T convert(byte from, Class toType); + + /** + * @throws IllegalArgumentException if conversion is not supported + */ + public abstract T convert(short from, Class toType); + + /** + * @throws IllegalArgumentException if conversion is not supported + */ + public abstract T convert(int from, Class toType); + + /** + * @throws IllegalArgumentException if conversion is not supported + */ + public abstract T convert(long from, Class toType); + + /** + * @throws IllegalArgumentException if conversion is not supported + */ + public abstract T convert(float from, Class toType); + + /** + * @throws IllegalArgumentException if conversion is not supported + */ + public abstract T convert(double from, Class toType); + + /** + * @throws IllegalArgumentException if conversion is not supported + */ + public abstract T convert(boolean from, Class toType); + + /** + * @throws IllegalArgumentException if conversion is not supported + */ + public abstract T convert(char from, Class 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 { + private final Class mConverterType; + + // Map "from class" to "to class" to optional conversion method. + private final Map> 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 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>(); + + // Add built-in primitive boxing/unboxing conversions. + for (Class[] tuple : mBoxMatrix) { + Map to = new HashMap(); + 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 to = mConvertMap.get(params[0]); + if (to == null) { + to = new HashMap(); + mConvertMap.put(params[0], to); + } + + to.put(toType, m); + } + + // Add automatic widening conversions. + + // Copy to prevent concurrent modification. + Map> convertMap = + new HashMap>(mConvertMap); + + for (Map.Entry> entry : convertMap.entrySet()) { + Class fromType = entry.getKey(); + + // Copy to prevent concurrent modification. + Map toMap = new HashMap(entry.getValue()); + + for (Map.Entry to : toMap.entrySet()) { + Class toType = to.getKey(); + Method conversionMethod = to.getValue(); + addAutomaticConversion(fromType, toType, conversionMethod); + } + } + + /* + for (Map.Entry> entry : mConvertMap.entrySet()) { + Class fromType = entry.getKey(); + for (Map.Entry 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 toMap; + Map caseMap; + + if (fromType == null) { + Map> convertMap = + new HashMap>(mConvertMap); + // Remove primitive type cases, since they will never match. + Iterator 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[] caseMatches = caseMatches(caseMap, caseCount); + + for (int i=0; i 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 matches = caseMatches[i]; + if (matches == null || matches.size() == 0) { + continue; + } + + switchLabels[i].setLocation(); + + int matchCount = matches.size(); + for (int j=0; j[] caseMatches(Map caseMap, int caseCount) { + List[] 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(); + } + 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 to = mConvertMap.get(fromType); + if (to == null) { + to = new HashMap(); + 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