/* * 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.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; 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(); /** * @param converterType type of converter to generate * @throws IllegalArgumentException if converter doesn't a no-arg constructor */ public static C build(Class 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 Class buildClass (Class converterType) { Class converterClass = (Class) cCache.get(converterType); if (converterClass == null) { converterClass = new Builder(converterType).buildClass(); cCache.put(converterType, converterClass); } return converterClass; } /** * @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); } 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); } } */ } Class 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 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; } Map> caseMatches = new HashMap>(); for (Class to : caseMap.keySet()) { int caseValue = to.hashCode(); List matches = caseMatches.get(caseValue); if (matches == null) { matches = new ArrayList(); 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 matches : caseMatches.values()) { switchLabels[i].setLocation(); int matchCount = matches.size(); for (int j=0; j 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; } } }