All Downloads are FREE. Search and download functionalities are using the official Maven repository.

mockit.internal.expectations.injection.TestedClassInstantiations Maven / Gradle / Ivy

Go to download

JMockit is a Java toolkit for developer (unit/integration) testing. It contains mocking APIs and other tools, supporting both JUnit and TestNG. The mocking APIs allow all kinds of Java code, without testability restrictions, to be tested in isolation from selected dependencies.

There is a newer version: 1.7
Show newest version
/*
 * 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 INJECT_CLASS;

   static
   {
      Class injectClass;
      ClassLoader cl = TestedClassInstantiations.class.getClassLoader();

      try {
         //noinspection unchecked
         injectClass = (Class) Class.forName("javax.inject.Inject", false, cl);
      }
      catch (ClassNotFoundException ignore) { injectClass = null; }

      INJECT_CLASS = injectClass;
   }

   private final List testedFields;
   private final List injectableFields;
   private List injectables;
   private final List consumedInjectables;
   private Object testClassInstance;
   private Type typeOfInjectionPoint;

   private final class TestedField
   {
      final Field testedField;
      private TestedObjectCreation testedObjectCreation;
      private List 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();
      injectableFields = new ArrayList();
      consumedInjectables = new ArrayList();
   }

   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(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() { public Object get() { return value; } };
      }

      return value;
   }

   private final class TestedObjectCreation
   {
      private final Class declaredClass;
      private final Class actualClass;
      private Constructor constructor;
      private List 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();
         }

         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 injectablesFound = findAvailableInjectablesForConstructor(c);

                  if (injectablesFound != null) {
                     injectablesForConstructor = injectablesFound;
                     constructor = c;
                  }

                  return true;
               }
            }

            return false;
         }

         private void findSatisfiedConstructorWithMostParameters(Constructor[] constructors)
         {
            Arrays.sort(constructors, new Comparator>() {
               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 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 findAvailableInjectablesForConstructor(Constructor candidate)
         {
            parameterTypes = candidate.getGenericParameterTypes();
            int n = parameterTypes.length;
            List injectablesFound = new ArrayList(n);
            boolean varArgs = candidate.isVarArgs();

            if (varArgs) {
               n--;
            }

            String constructorDesc = "" + 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 varargValues = new ArrayList();
            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 = "" + 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 findAllTargetInstanceFieldsInTestedClassHierarchy()
      {
         List targetFields = new ArrayList();
         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 targetFields)
      {
         if (!requiresJavaxInject && foundJavaxInject) {
            ListIterator 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 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 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 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;
      }
   }
}