/** * Copyright 2010 Wealthfront Inc. 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.kaching.platform.converters; import static com.kaching.platform.converters.CollectionOfElementsConverter.COLLECTION_KINDS; import static com.kaching.platform.converters.InstantiatorErrors.cannotAnnotateOptionWithOptional; import static com.kaching.platform.converters.InstantiatorErrors.cannotSpecifyDefaultValueAndConstant; import static com.kaching.platform.converters.InstantiatorErrors.constantHasIncompatibleType; import static com.kaching.platform.converters.InstantiatorErrors.constantIsNotStaticFinal; import static com.kaching.platform.converters.InstantiatorErrors.enumHasAmbiguousNames; import static com.kaching.platform.converters.InstantiatorErrors.illegalConstructor; import static com.kaching.platform.converters.InstantiatorErrors.incorrectBoundForConverter; import static com.kaching.platform.converters.InstantiatorErrors.incorrectDefaultValue; import static com.kaching.platform.converters.InstantiatorErrors.moreThanOneConstructor; import static com.kaching.platform.converters.InstantiatorErrors.moreThanOneConstructorWithInstantiate; import static com.kaching.platform.converters.InstantiatorErrors.moreThanOneMatchingFunction; import static com.kaching.platform.converters.InstantiatorErrors.noConverterForType; import static com.kaching.platform.converters.InstantiatorErrors.noSuchField; import static com.kaching.platform.converters.InstantiatorErrors.optionalLiteralParameterMustHaveDefault; import static com.kaching.platform.converters.InstantiatorErrors.unableToGetField; import static com.kaching.platform.converters.InstantiatorErrors.unableToInstantiate; import static com.kaching.platform.converters.InstantiatorErrors.unableToResolveConstant; import static com.kaching.platform.converters.InstantiatorErrors.unableToResolveFullyQualifiedConstant; import static com.kaching.platform.converters.NativeConverters.C_BOOLEAN; import static com.kaching.platform.converters.NativeConverters.C_BYTE; import static com.kaching.platform.converters.NativeConverters.C_CHAR; import static com.kaching.platform.converters.NativeConverters.C_DOUBLE; import static com.kaching.platform.converters.NativeConverters.C_FLOAT; import static com.kaching.platform.converters.NativeConverters.C_INT; import static com.kaching.platform.converters.NativeConverters.C_LONG; import static com.kaching.platform.converters.NativeConverters.C_SHORT; import static com.kaching.platform.converters.NativeConverters.C_STRING; import static com.kaching.platform.converters.Optional.VALUE_DEFAULT; import static java.lang.String.format; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; import java.util.BitSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.collect.ImmutableMap; import com.google.inject.TypeLiteral; import com.kaching.platform.common.Errors; import com.kaching.platform.common.Option; import com.kaching.platform.common.types.Types; import com.kaching.platform.common.types.Unification; import com.kaching.platform.converters.ConstructorAnalysis.AnalysisResult; import com.kaching.platform.converters.ConstructorAnalysis.FormalParameter; class InstantiatorImplFactory<T> { private final static ImmutableMap<Type, Converter<?>> BASE_CONVERTERS = ImmutableMap.<Type, Converter<?>> builder() .put(String.class, C_STRING) .put(Boolean.TYPE, C_BOOLEAN) .put(Byte.TYPE, C_BYTE) .put(Character.TYPE, C_CHAR) .put(Double.TYPE, C_DOUBLE) .put(Float.TYPE, C_FLOAT) .put(Integer.TYPE, C_INT) .put(Long.TYPE, C_LONG) .put(Short.TYPE, C_SHORT) .build(); private final Errors errors; private final ConverterBinderImpl binder; private final Class<T> klass; private InstantiatorImplFactory(Errors errors, Class<T> klass) { this.errors = errors; this.klass = klass; this.binder = new ConverterBinderImpl(errors); } static <T> InstantiatorImplFactory<T> createFactory(Errors errors, Class<T> klass) { return new InstantiatorImplFactory<T>(errors, klass); } ConverterBinder binder() { return binder; } Option<InstantiatorImpl<T>> build() { // 1. find constructor for (Constructor<T> constructor : getConstructor()) { constructor.setAccessible(true); // 2. for each parameter, find converter Type[] genericParameterTypes = constructor.getGenericParameterTypes(); Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); int parametersCount = genericParameterTypes.length; Converter<?>[] converters = parametersCount == 0 ? null : new Converter<?>[parametersCount]; BitSet optionality = new BitSet(); BitSet wrapInOption = new BitSet(); String[] defaultValues = null; Object[] defaultConstants = null; next_parameter: for (int i = 0; i < parametersCount; i++) { Annotation[] annotations = parameterAnnotations[i]; Type genericParameterType = genericParameterTypes[i]; Type genericParameterTypeForConverter; if (genericParameterType instanceof ParameterizedType && ((ParameterizedType) genericParameterType).getRawType().equals(Option.class)) { wrapInOption.set(i); genericParameterTypeForConverter = ((ParameterizedType) genericParameterType).getActualTypeArguments()[0]; } else { genericParameterTypeForConverter = genericParameterType; } for (final Converter<?> converter : createConverter( genericParameterTypeForConverter)) { converters[i] = converter; for (Optional optional : getOptionalAnnotation(annotations)) { if (wrapInOption.get(i)) { cannotAnnotateOptionWithOptional(errors, genericParameterType); continue next_parameter; } String defaultValue = optional.value(); String defaultConstant = optional.constant(); if (!defaultValue.equals(VALUE_DEFAULT) && !defaultConstant.isEmpty()) { cannotSpecifyDefaultValueAndConstant(errors, optional); continue next_parameter; } else if (!defaultValue.equals(VALUE_DEFAULT)) { try { converter.fromString(defaultValue); if (defaultValues == null) { defaultValues = new String[parametersCount]; } defaultValues[i] = defaultValue; } catch (RuntimeException e) { incorrectDefaultValue(errors, defaultValue, e); } } else if (!defaultConstant.isEmpty()) { String[] parts = defaultConstant.split("#"); Class<?> container; String constantName; switch (parts.length) { case 1: container = klass; constantName = parts[0]; break; case 2: try { container = Class.forName(parts[0]); } catch (ClassNotFoundException e) { unableToResolveFullyQualifiedConstant(errors, defaultConstant); continue next_parameter; } constantName = parts[1]; break; default: unableToResolveFullyQualifiedConstant(errors, defaultConstant); continue next_parameter; } Field fieldOfConstant; try { fieldOfConstant = container.getDeclaredField(constantName); } catch (SecurityException e) { // TODO(pascal): better handling? throw new RuntimeException(e); } catch (NoSuchFieldException e) { unableToResolveConstant(errors, container, defaultConstant); continue next_parameter; } if ((fieldOfConstant.getModifiers() & Modifier.STATIC) == 0 || (fieldOfConstant.getModifiers() & Modifier.FINAL) == 0) { constantIsNotStaticFinal(errors, container, constantName); continue next_parameter; } // TODO(pascal): improve check. Should use MoreTypes.isAssignableFrom. if (!genericParameterType.equals(fieldOfConstant.getType())) { constantHasIncompatibleType(errors, container, constantName); continue next_parameter; } if (defaultConstants == null) { defaultConstants = new Object[parametersCount]; } try { defaultConstants[i] = fieldOfConstant.get(null); } catch (IllegalArgumentException e) { // TODO(pascal): better handling? throw new RuntimeException(e); } catch (IllegalAccessException e) { // TODO(pascal): better handling? throw new RuntimeException(e); } } else { // optional literal types are not allowed to omit default values if (BASE_CONVERTERS.containsKey(genericParameterType) && !String.class.equals(genericParameterType)) { optionalLiteralParameterMustHaveDefault(errors, i); } } optionality.set(i); } } } // 3. reverse mapping (fields to parameters) Field[] fields = null; AnalysisResult analysisResult = null; try { analysisResult = ConstructorAnalysis.analyse(klass, constructor); fields = retrieveFieldsFromAssignment( parametersCount, analysisResult.assignments); } catch (IOException e) { throw new IllegalStateException("should be able to access the class"); } catch (ConstructorAnalysis.IllegalConstructorException e) { illegalConstructor(errors, klass, e.getMessage()); } // 4. done if (!errors.hasErrors()) { return Option.some(new InstantiatorImpl<T>( constructor, converters, fields, optionality, wrapInOption, defaultValues, defaultConstants, analysisResult.paramaterNames)); } else { return Option.none(); } } return Option.none(); } private Option<Optional> getOptionalAnnotation(Annotation[] annotations) { for (Annotation annotation : annotations) { if (annotation instanceof Optional) { return Option.some((Optional) annotation); } } return Option.none(); } @SuppressWarnings({ "unchecked", "rawtypes" }) Option<? extends Converter<?>> createConverter(Type targetType) { int sizeBefore = errors.size(); // 1. explicit binding Map<TypeLiteral<?>, Converter<?>> instances = binder.getInstances(); Map<TypeLiteral<?>, Class<? extends Converter<?>>> bindings = binder.getBindings(); List<Function<Type, Option<? extends Converter<?>>>> functions = binder.getFunctions(); if (instances != null) { for (Entry<TypeLiteral<?>, Converter<?>> entry : instances.entrySet()) { if (Types.isInstance(entry.getKey().getType(), targetType)) { return Option.some(entry.getValue()); } } } if (bindings != null) { for (Entry<TypeLiteral<?>, Class<? extends Converter<?>>> entry : bindings.entrySet()) { if (Types.isInstance(entry.getKey().getType(), targetType)) { for (Converter<?> converter : instantiateConverter(entry.getValue(), targetType)) { return Option.some(converter); } } } } // 2. function Converter<?> foundConverter = null; for (Function<Type, Option<? extends Converter<?>>> function : functions) { Option<? extends Converter<?>> option = function.apply(targetType); if (option.isDefined()) { if (foundConverter == null) { foundConverter = option.getOrThrow(); } else { moreThanOneMatchingFunction(errors, targetType); } } } if (foundConverter != null) { return Option.some(foundConverter); } // 3. @ConvertedBy if (targetType instanceof Class || targetType instanceof ParameterizedType) { Class targetClass = (Class) (targetType instanceof Class ? targetType : ((ParameterizedType) targetType).getRawType()); for (Converter<?> converter : createConverterUsingConvertedBy(targetType, targetClass)) { return Option.some(converter); } } if (targetType instanceof Class) { Class targetClass = (Class) targetType; // 4. base converters if (BASE_CONVERTERS.containsKey(targetClass)) { return Option.some(BASE_CONVERTERS.get(targetClass)); } // 5. has <init>(Ljava/lang/String;)V; for (Converter<?> converter : createConverterUsingStringConstructor(targetClass)) { return Option.some(converter); } // 6. is an Enum if (Enum.class.isAssignableFrom(targetClass)) { try { return Option.some((Converter<?>) new EnumConverter(targetClass)); } catch (IllegalArgumentException e) { enumHasAmbiguousNames(errors, targetClass); } } } else if (targetType instanceof ParameterizedType) { // 7. has <init>(Ljava/lang/String;)V; for (Converter<?> converter : createConverterUsingStringConstructor( (Class)((ParameterizedType) targetType).getRawType())) { return Option.some(converter); } // 8. Set, List, Collection ParameterizedType parameterizedTargetType = (ParameterizedType) targetType; if (COLLECTION_KINDS.containsKey(parameterizedTargetType.getRawType()) && parameterizedTargetType.getActualTypeArguments().length == 1) { Option<? extends Converter<?>> maybeElementConverter = createConverter( parameterizedTargetType.getActualTypeArguments()[0]); if (maybeElementConverter.isDefined()) { return (Option) Option.some(new CollectionOfElementsConverter( parameterizedTargetType.getRawType(), maybeElementConverter.getOrThrow())); } } else { // TODO(pascal) provide more detailed errors such as "you need to // parameterize your list" } } if (sizeBefore == errors.size()) { noConverterForType(errors, targetType); } return Option.none(); } @VisibleForTesting Field[] retrieveFieldsFromAssignment( int parametersCount, Map<String, FormalParameter> assignments) { Field[] fields = new Field[parametersCount]; outer: for (Entry<String, FormalParameter> entry : assignments.entrySet()) { int parameterIndex = entry.getValue().getIndex(); if (parameterIndex < 0 || fields.length <= parameterIndex) { throw new IllegalStateException( format("formal parameter out of bounds (index %s)", parameterIndex)); } Field field = null; String fieldName = entry.getKey(); Class<?> classSearched = klass; while (true) { try { field = classSearched.getDeclaredField(fieldName); field.setAccessible(true); fields[parameterIndex] = field; continue outer; } catch (SecurityException e) { unableToGetField(errors, fieldName, e); continue outer; } catch (NoSuchFieldException e) { if (classSearched.equals(Object.class)) { noSuchField(errors, fieldName); continue outer; } else { classSearched = classSearched.getSuperclass(); } } } } return fields; } private Option<? extends Converter<?>> createConverterUsingConvertedBy( final Type targetType, Class<?> targetClass) { Annotation[] typeAnnotations = targetClass.getAnnotations(); for (Annotation typeAnnotation : typeAnnotations) { if (typeAnnotation instanceof ConvertedBy) { Class<? extends Converter<?>> converterClass = ((ConvertedBy) typeAnnotation).value(); for (Converter<?> converter : instantiateConverter(converterClass, targetType)) { return Option.some(converter); } } } return Option.none(); } private Option<? extends Converter<?>> createConverterUsingStringConstructor( final Class<?> targetClass) { Constructor<?> stringConstructor; try { stringConstructor = targetClass.getDeclaredConstructor(String.class); } catch (SecurityException e) { // do proper exception handling, add this to errors throw new RuntimeException(e); } catch (NoSuchMethodException e) { // Not having a String constructor is an acceptable outcome. return Option.none(); } return Option.some(new StringConstructorConverter<Object>(stringConstructor)); } private Option<? extends Converter<?>> instantiateConverter( Class<? extends Converter<?>> converterClass, Type targetType) { try { Type producedType = Unification.getActualTypeArgument(converterClass, Converter.class, 0); if (Types.isInstance(producedType, targetType)) { Constructor<? extends Converter<?>> ctor = converterClass.getDeclaredConstructor(); ctor.setAccessible(true); return Option.some(ctor.newInstance()); } else { incorrectBoundForConverter(errors, targetType, converterClass, producedType); } } catch (InstantiationException e) { unableToInstantiate(errors, converterClass, e); } catch (IllegalAccessException e) { unableToInstantiate(errors, converterClass, e); } catch (SecurityException e) { unableToInstantiate(errors, converterClass, e); } catch (NoSuchMethodException e) { unableToInstantiate(errors, converterClass, e); } catch (IllegalArgumentException e) { unableToInstantiate(errors, converterClass, e); } catch (InvocationTargetException e) { unableToInstantiate(errors, converterClass, e); } return Option.none(); } @VisibleForTesting Option<Constructor<T>> getConstructor() { @SuppressWarnings("unchecked") Constructor<T>[] constructors = (Constructor<T>[]) klass.getDeclaredConstructors(); if (constructors.length > 1) { Constructor<T> convertableConstructor = null; for (Constructor<T> constructor : constructors) { if (constructor.getAnnotation(Instantiate.class) != null) { if (convertableConstructor == null) { convertableConstructor = constructor; } else { moreThanOneConstructorWithInstantiate(errors, klass); return Option.none(); } } } if (convertableConstructor != null) { return Option.some(convertableConstructor); } else { moreThanOneConstructor(errors, klass); return Option.none(); } } else if (constructors.length == 0) { InstantiatorErrors.noConstructorFound(errors, klass); return Option.none(); } else { return Option.some(constructors[0]); } } Errors getErrors() { return errors; } }