/* * Copyright (c) 2006-2012 Rogério Liesenfeld * This file is subject to the terms of the MIT license (see LICENSE.txt). */ package mockit.internal.expectations.injection; import java.lang.annotation.*; import java.lang.reflect.*; import java.lang.reflect.Type; import java.util.*; import javax.inject.*; import static java.lang.reflect.Modifier.*; import mockit.*; import mockit.external.asm4.*; import mockit.internal.*; import mockit.internal.expectations.mocking.*; import mockit.internal.state.*; import mockit.internal.util.*; import static mockit.internal.util.Utilities.*; public final class TestedClassInstantiations { private static final Class<? extends Annotation> INJECT_CLASS; static { Class<? extends Annotation> injectClass; ClassLoader cl = TestedClassInstantiations.class.getClassLoader(); try { //noinspection unchecked injectClass = (Class<? extends Annotation>) Class.forName("javax.inject.Inject", false, cl); } catch (ClassNotFoundException ignore) { injectClass = null; } INJECT_CLASS = injectClass; } private final List<TestedField> testedFields; private final List<MockedType> injectableFields; private List<MockedType> injectables; private final List<MockedType> consumedInjectables; private Object testClassInstance; private Type typeOfInjectionPoint; private final class TestedField { final Field testedField; private TestedObjectCreation testedObjectCreation; private List<Field> targetFields; TestedField(Field field) { testedField = field; } void instantiateWithInjectableValues() { Object testedObject = FieldReflection.getFieldValue(testedField, testClassInstance); boolean requiresJavaxInject = false; Class<?> testedClass; if (testedObject == null && !isFinal(testedField.getModifiers())) { if (testedObjectCreation == null) { testedObjectCreation = new TestedObjectCreation(testedField); } testedClass = testedObjectCreation.declaredClass; testedObject = testedObjectCreation.create(); FieldReflection.setFieldValue(testedField, testClassInstance, testedObject); requiresJavaxInject = testedObjectCreation.constructorAnnotatedWithJavaxInject; } else { testedClass = testedObject == null ? null : testedObject.getClass(); } if (testedObject != null) { FieldInjection fieldInjection = new FieldInjection(testedClass, testedObject, requiresJavaxInject); if (targetFields == null) { targetFields = fieldInjection.findAllTargetInstanceFieldsInTestedClassHierarchy(); } fieldInjection.injectIntoEligibleFields(targetFields); } } } public TestedClassInstantiations() { testedFields = new LinkedList<TestedField>(); injectableFields = new ArrayList<MockedType>(); consumedInjectables = new ArrayList<MockedType>(); } public boolean findTestedAndInjectableFields(Class<?> testClass) { new ParameterNameExtractor(true).extractNames(testClass); Field[] fieldsInTestClass = testClass.getDeclaredFields(); for (Field field : fieldsInTestClass) { if (field.isAnnotationPresent(Tested.class)) { testedFields.add(new TestedField(field)); } else { MockedType mockedType = new MockedType(field, true); if (mockedType.injectable) { injectableFields.add(mockedType); } } } return !testedFields.isEmpty(); } public void assignNewInstancesToTestedFields(Object testClassInstance) { this.testClassInstance = testClassInstance; buildListsOfInjectables(); for (TestedField testedField : testedFields) { testedField.instantiateWithInjectableValues(); consumedInjectables.clear(); } } private void buildListsOfInjectables() { ParameterTypeRedefinitions paramTypeRedefs = TestRun.getExecutingTest().getParameterTypeRedefinitions(); if (paramTypeRedefs == null) { injectables = injectableFields; } else { injectables = new ArrayList<MockedType>(injectableFields); injectables.addAll(paramTypeRedefs.getInjectableParameters()); } } void setTypeOfInjectionPoint(Type parameterOrFieldType) { typeOfInjectionPoint = parameterOrFieldType; } boolean hasSameTypeAsInjectionPoint(MockedType injectable) { return isSameTypeAsInjectionPoint(injectable.declaredType); } boolean isSameTypeAsInjectionPoint(Type injectableType) { if (typeOfInjectionPoint.equals(injectableType)) return true; if (INJECT_CLASS != null && typeOfInjectionPoint instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) typeOfInjectionPoint; if (parameterizedType.getRawType() == Provider.class) { Type providedType = parameterizedType.getActualTypeArguments()[0]; return providedType.equals(injectableType); } } return false; } private Object getValueToInject(MockedType injectable) { if (consumedInjectables.contains(injectable)) { return null; } Object value = injectable.getValueToInject(testClassInstance); if (value != null) { consumedInjectables.add(injectable); } return value; } private Object wrapInProviderIfNeeded(Type type, final Object value) { if ( INJECT_CLASS != null && type instanceof ParameterizedType && !(value instanceof Provider) && ((ParameterizedType) type).getRawType() == Provider.class ) { return new Provider<Object>() { public Object get() { return value; } }; } return value; } private final class TestedObjectCreation { private final Class<?> declaredClass; private final Class<?> actualClass; private Constructor<?> constructor; private List<MockedType> injectablesForConstructor; private Type[] parameterTypes; boolean constructorAnnotatedWithJavaxInject; TestedObjectCreation(Field testedField) { declaredClass = testedField.getType(); actualClass = isAbstract(declaredClass.getModifiers()) ? generateSubclass(testedField.getGenericType()) : declaredClass; } private Class<?> generateSubclass(Type testedType) { ClassReader classReader = ClassFile.createReaderOrGetFromCache(declaredClass); String subclassName = GeneratedClasses.getNameForGeneratedClass(declaredClass); ClassVisitor modifier = new SubclassGenerationModifier(testedType, classReader, subclassName); classReader.accept(modifier, 0); return new ImplementationClass().defineNewClass(declaredClass.getClassLoader(), modifier, subclassName); } Object create() { new ConstructorSearch().findConstructorAccordingToAccessibilityAndAvailableInjectables(); if (constructor == null) { throw new IllegalArgumentException( "No constructor in " + declaredClass + " that can be satisfied by available injectables"); } return new ConstructorInjection().instantiate(); } MockedType findNextInjectableForVarargsParameter() { for (MockedType injectable : injectables) { if (hasSameTypeAsInjectionPoint(injectable) && !consumedInjectables.contains(injectable)) { return injectable; } } return null; } private final class ConstructorSearch { private final String testedClassDesc; ConstructorSearch() { testedClassDesc = new ParameterNameExtractor(false).extractNames(declaredClass); injectablesForConstructor = new ArrayList<MockedType>(); } void findConstructorAccordingToAccessibilityAndAvailableInjectables() { constructor = null; Constructor<?>[] constructors = actualClass.getDeclaredConstructors(); if (INJECT_CLASS != null && findSingleInjectAnnotatedConstructor(constructors)) { constructorAnnotatedWithJavaxInject = true; } else { findSatisfiedConstructorWithMostParameters(constructors); } } private boolean findSingleInjectAnnotatedConstructor(Constructor<?>[] constructors) { for (Constructor<?> c : constructors) { if (c.isAnnotationPresent(INJECT_CLASS)) { List<MockedType> injectablesFound = findAvailableInjectablesForConstructor(c); if (injectablesFound != null) { injectablesForConstructor = injectablesFound; constructor = c; } return true; } } return false; } private void findSatisfiedConstructorWithMostParameters(Constructor<?>[] constructors) { Arrays.sort(constructors, new Comparator<Constructor<?>>() { public int compare(Constructor<?> c1, Constructor<?> c2) { int m1 = constructorModifiers(c1); int m2 = constructorModifiers(c2); if (m1 == m2) return 0; if (m1 == PUBLIC) return -1; if (m2 == PUBLIC) return 1; if (m1 == PROTECTED) return -1; if (m2 == PROTECTED) return 1; if (m2 == PRIVATE) return -1; return 1; } }); for (Constructor<?> c : constructors) { List<MockedType> injectablesFound = findAvailableInjectablesForConstructor(c); if ( injectablesFound != null && (constructor == null || constructorModifiers(c) == constructorModifiers(constructor) && injectablesFound.size() >= injectablesForConstructor.size()) ) { injectablesForConstructor = injectablesFound; constructor = c; } } } private static final int CONSTRUCTOR_ACCESS = PUBLIC + PROTECTED + PRIVATE; private int constructorModifiers(Constructor<?> c) { return CONSTRUCTOR_ACCESS & c.getModifiers(); } private List<MockedType> findAvailableInjectablesForConstructor(Constructor<?> candidate) { parameterTypes = candidate.getGenericParameterTypes(); int n = parameterTypes.length; List<MockedType> injectablesFound = new ArrayList<MockedType>(n); boolean varArgs = candidate.isVarArgs(); if (varArgs) { n--; } String constructorDesc = "<init>" + mockit.external.asm4.Type.getConstructorDescriptor(candidate); for (int i = 0; i < n; i++) { setTypeOfInjectionPoint(parameterTypes[i]); String parameterName = ParameterNames.getName(testedClassDesc, constructorDesc, i); MockedType injectable = parameterName == null ? null : findInjectable(parameterName); if (injectable == null || injectablesFound.contains(injectable)) { return null; } injectablesFound.add(injectable); } if (varArgs) { MockedType injectable = hasInjectedValuesForVarargsParameter(n); if (injectable != null) { injectablesFound.add(injectable); } } return injectablesFound; } private MockedType findInjectable(String nameOfInjectionPoint) { boolean multipleInjectablesFound = false; MockedType found = null; for (MockedType injectable : injectables) { if (hasSameTypeAsInjectionPoint(injectable)) { if (found == null) { found = injectable; } else { if (nameOfInjectionPoint.equals(injectable.mockId)) { return injectable; } multipleInjectablesFound = true; } } } if (multipleInjectablesFound && !nameOfInjectionPoint.equals(found.mockId)) { return null; } return found; } private MockedType hasInjectedValuesForVarargsParameter(int varargsParameterIndex) { getTypeOfInjectionPointFromVarargsParameter(varargsParameterIndex); return findNextInjectableForVarargsParameter(); } } private Type getTypeOfInjectionPointFromVarargsParameter(int varargsParameterIndex) { Type parameterType = parameterTypes[varargsParameterIndex]; if (parameterType instanceof Class<?>) { parameterType = ((Class<?>) parameterType).getComponentType(); } else { parameterType = ((GenericArrayType) parameterType).getGenericComponentType(); } setTypeOfInjectionPoint(parameterType); return parameterType; } private final class ConstructorInjection { Object instantiate() { parameterTypes = constructor.getGenericParameterTypes(); int n = parameterTypes.length; Object[] arguments = new Object[n]; boolean varArgs = constructor.isVarArgs(); if (varArgs) { n--; } for (int i = 0; i < n; i++) { MockedType injectable = injectablesForConstructor.get(i); Object value = getArgumentValueToInject(injectable); arguments[i] = wrapInProviderIfNeeded(parameterTypes[i], value); } if (varArgs) { arguments[n] = obtainInjectedVarargsArray(n); } return ConstructorReflection.invoke(constructor, arguments); } private Object obtainInjectedVarargsArray(int varargsParameterIndex) { Type varargsElementType = getTypeOfInjectionPointFromVarargsParameter(varargsParameterIndex); List<Object> varargValues = new ArrayList<Object>(); MockedType injectable; while ((injectable = findNextInjectableForVarargsParameter()) != null) { Object value = getValueToInject(injectable); if (value != null) { value = wrapInProviderIfNeeded(varargsElementType, value); varargValues.add(value); } } int elementCount = varargValues.size(); Object varargArray = Array.newInstance(getClassType(varargsElementType), elementCount); for (int i = 0; i < elementCount; i++) { Array.set(varargArray, i, varargValues.get(i)); } return varargArray; } private Object getArgumentValueToInject(MockedType injectable) { Object argument = getValueToInject(injectable); if (argument == null) { throw new IllegalArgumentException( "No injectable value available" + missingInjectableDescription(injectable.mockId)); } return argument; } private String missingInjectableDescription(String name) { String classDesc = mockit.external.asm4.Type.getInternalName(constructor.getDeclaringClass()); String constructorDesc = "<init>" + mockit.external.asm4.Type.getConstructorDescriptor(constructor); String constructorDescription = new MethodFormatter(classDesc, constructorDesc).toString(); return " for parameter \"" + name + "\" in constructor " + constructorDescription.replace("java.lang.", ""); } } } private final class FieldInjection { private final Class<?> testedClass; private final Object testedObject; private final boolean requiresJavaxInject; private boolean foundJavaxInject; private FieldInjection(Class<?> testedClass, Object testedObject, boolean requiresJavaxInject) { this.testedClass = testedClass; this.testedObject = testedObject; this.requiresJavaxInject = requiresJavaxInject; } List<Field> findAllTargetInstanceFieldsInTestedClassHierarchy() { List<Field> targetFields = new ArrayList<Field>(); Class<?> classWithFields = testedClass; do { Field[] fields = classWithFields.getDeclaredFields(); for (Field field : fields) { if (isEligibleForInjection(field)) { targetFields.add(field); } } classWithFields = classWithFields.getSuperclass(); } while (isFromSameModuleOrSystemAsSuperClass(classWithFields)); discardFieldsNotAnnotatedWithJavaxInjectIfAtLeastOneIsAnnotated(targetFields); return targetFields; } private boolean isEligibleForInjection(Field field) { if (isFinal(field.getModifiers())) return false; if (requiresJavaxInject) return isAnnotatedWithJavaxInject(field); boolean notStatic = !isStatic(field.getModifiers()); return INJECT_CLASS == null ? notStatic : isAnnotatedWithJavaxInject(field) || notStatic; } private boolean isAnnotatedWithJavaxInject(Field field) { boolean annotated = field.isAnnotationPresent(INJECT_CLASS); if (annotated) foundJavaxInject = true; return annotated; } private void discardFieldsNotAnnotatedWithJavaxInjectIfAtLeastOneIsAnnotated(List<Field> targetFields) { if (!requiresJavaxInject && foundJavaxInject) { ListIterator<Field> itr = targetFields.listIterator(); while (itr.hasNext()) { Field targetField = itr.next(); if (!targetField.isAnnotationPresent(INJECT_CLASS)) { itr.remove(); } } } } private boolean isFromSameModuleOrSystemAsSuperClass(Class<?> superClass) { if (superClass.getClassLoader() == null) { return false; } if (superClass.getProtectionDomain() == testedClass.getProtectionDomain()) { return true; } String className1 = superClass.getName(); String className2 = testedClass.getName(); int p1 = className1.indexOf('.'); int p2 = className2.indexOf('.'); if (p1 != p2 || p1 == -1) { return false; } p1 = className1.indexOf('.', p1 + 1); p2 = className2.indexOf('.', p2 + 1); return p1 == p2 && p1 > 0 && className1.substring(0, p1).equals(className2.substring(0, p2)); } void injectIntoEligibleFields(List<Field> targetFields) { for (Field field : targetFields) { if (notAssignedByConstructor(field)) { Object injectableValue = getValueForFieldIfAvailable(targetFields, field); if (injectableValue != null) { injectableValue = wrapInProviderIfNeeded(field.getGenericType(), injectableValue); FieldReflection.setFieldValue(field, testedObject, injectableValue); } } } } private boolean notAssignedByConstructor(Field field) { if (INJECT_CLASS != null && field.isAnnotationPresent(INJECT_CLASS)) { return true; } Object fieldValue = FieldReflection.getFieldValue(field, testedObject); if (fieldValue == null) { return true; } Class<?> fieldType = field.getType(); if (!fieldType.isPrimitive()) { return false; } Object defaultValue = DefaultValues.defaultValueForPrimitiveType(fieldType); return fieldValue.equals(defaultValue); } private Object getValueForFieldIfAvailable(List<Field> targetFields, Field fieldToBeInjected) { setTypeOfInjectionPoint(fieldToBeInjected.getGenericType()); String targetFieldName = fieldToBeInjected.getName(); MockedType mockedType; if (withMultipleTargetFieldsOfSameType(targetFields, fieldToBeInjected)) { mockedType = findInjectableByTypeAndName(targetFieldName); } else { mockedType = findInjectableByTypeAndOptionallyName(targetFieldName); } return mockedType == null ? null : getValueToInject(mockedType); } private boolean withMultipleTargetFieldsOfSameType(List<Field> targetFields, Field fieldToBeInjected) { for (Field targetField : targetFields) { if (targetField != fieldToBeInjected && isSameTypeAsInjectionPoint(targetField.getGenericType())) { return true; } } return false; } private MockedType findInjectableByTypeAndName(String targetFieldName) { for (MockedType injectable : injectables) { if (hasSameTypeAsInjectionPoint(injectable) && targetFieldName.equals(injectable.mockId)) { return injectable; } } return null; } private MockedType findInjectableByTypeAndOptionallyName(String targetFieldName) { MockedType found = null; for (MockedType injectable : injectables) { if (hasSameTypeAsInjectionPoint(injectable)) { if (targetFieldName.equals(injectable.mockId)) { return injectable; } if (found == null) { found = injectable; } } } return found; } } }