/* * Copyright 2008-2012 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.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; 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; /** * 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 SoftValuedCache<Class, Class<? extends Converter>> cCache = SoftValuedCache.newCache(7); /** * @param converterType type of converter to generate * @throws IllegalArgumentException if converter doesn't a no-arg constructor */ public static <C extends Converter> C build(Class<C> converterType) { try { return buildClass(converterType).newInstance(); } catch (InstantiationException e) { throw new IllegalArgumentException ("TypeConverter must have a public no-arg constructor: " + converterType); } catch (IllegalAccessException e) { // Not expected to happen, since generated constructors are public. throw new IllegalArgumentException(e); } } /** * @param converterType type of converter to generate */ public static synchronized <C extends Converter> Class<? extends C> buildClass (Class<C> converterType) { Class<? extends C> converterClass = (Class<? extends C>) cCache.get(converterType); if (converterClass == null) { converterClass = new Builder<C>(converterType).buildClass(); cCache.put(converterType, converterClass); } return converterClass; } /** * @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); } 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); } } */ } Class<? extends C> buildClass() { 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"); // Add constructors which match superclass. int ctorCount = 0; for (Constructor ctor : mConverterType.getDeclaredConstructors()) { int modifiers = ctor.getModifiers(); if (!Modifier.isPublic(modifiers) && !Modifier.isProtected(modifiers)) { continue; } ctorCount++; TypeDesc[] params = new TypeDesc[ctor.getParameterTypes().length]; for (int i=0; i<params.length; i++) { params[i] = TypeDesc.forClass(ctor.getParameterTypes()[i]); } MethodInfo mi = mClassFile.addConstructor(Modifiers.PUBLIC, params); CodeBuilder b = new CodeBuilder(mi); b.loadThis(); for (int i=0; i<params.length; i++) { b.loadLocal(b.getParameter(0)); } b.invokeSuperConstructor(params); b.returnVoid(); } if (ctorCount == 0) { throw new IllegalArgumentException ("TypeConverter has no public or protected constructors: " + mConverterType); } 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); } return ci.defineClass(mClassFile); } private void addPrimitiveConvertMethod(Class fromType) { Method m = getAbstractConvertMethod(fromType); if (m == null) { return; } CodeBuilder b = new CodeBuilder(mClassFile.addMethod(m)); addConversionSwitch(b, fromType); } 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; } Map<Integer, List<Class>> caseMatches = new HashMap<Integer, List<Class>>(); for (Class to : caseMap.keySet()) { int caseValue = to.hashCode(); List<Class> matches = caseMatches.get(caseValue); if (matches == null) { matches = new ArrayList<Class>(); caseMatches.put(caseValue, matches); } matches.add(to); } int[] cases = new int[caseMatches.size()]; Label[] switchLabels = new Label[caseMatches.size()]; Label noMatch = b.createLabel(); { int i = 0; for (Integer caseValue : caseMatches.keySet()) { cases[i] = caseValue; switchLabels[i] = b.createLabel(); i++; } } 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.switchBranch(cases, switchLabels, noMatch); } TypeDesc fromTypeDesc = TypeDesc.forClass(fromType); int i = 0; for (List<Class> matches : caseMatches.values()) { 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, toTypeDesc.toObjectType()); } b.returnValue(TypeDesc.OBJECT); } if (notEqual != null) { notEqual.setLocation(); } } i++; } 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(); } /** * @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) { if (method != null) { Class paramType = method.getParameterTypes()[0]; if (!paramType.isAssignableFrom(fromType)) { if (!fromType.isPrimitive() && paramType.isPrimitive()) { // Reject because unboxing could result in NullPointerException. return; } if (!new ConversionComparator(fromType).isConversionPossible(paramType)) { // Reject. return; } } Class returnType = method.getReturnType(); if (!toType.isAssignableFrom(returnType)) { if (!returnType.isPrimitive() && toType.isPrimitive()) { // Reject because unboxing could result in NullPointerException. return; } if (TypeDesc.forClass(returnType).toObjectType() != TypeDesc.forClass(toType).toObjectType()) { // Reject widening or narrowing return type. return; } } } 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; } } }