1/* 2 * Copyright (c) 2007 Mockito contributors 3 * This program is made available under the terms of the MIT License. 4 */ 5package org.mockito.internal.util.reflection; 6 7import org.mockito.exceptions.base.MockitoException; 8 9import java.lang.reflect.Constructor; 10import java.lang.reflect.Field; 11import java.lang.reflect.InvocationTargetException; 12import java.lang.reflect.Modifier; 13import java.util.Arrays; 14import java.util.Collections; 15import java.util.Comparator; 16import java.util.List; 17 18/** 19 * Initialize a field with type instance if a default constructor can be found. 20 * 21 * <p> 22 * If the given field is already initialized, then <strong>the actual instance is returned</strong>. 23 * This initializer doesn't work with inner classes, local classes, interfaces or abstract types. 24 * </p> 25 * 26 */ 27public class FieldInitializer { 28 29 private Object fieldOwner; 30 private Field field; 31 private ConstructorInstantiator instantiator; 32 33 34 /** 35 * Prepare initializer with the given field on the given instance. 36 * 37 * <p> 38 * This constructor fail fast if the field type cannot be handled. 39 * </p> 40 * 41 * @param fieldOwner Instance of the test. 42 * @param field Field to be initialize. 43 */ 44 public FieldInitializer(Object fieldOwner, Field field) { 45 this(fieldOwner, field, new NoArgConstructorInstantiator(fieldOwner, field)); 46 } 47 48 /** 49 * Prepare initializer with the given field on the given instance. 50 * 51 * <p> 52 * This constructor fail fast if the field type cannot be handled. 53 * </p> 54 * 55 * @param fieldOwner Instance of the test. 56 * @param field Field to be initialize. 57 * @param argResolver Constructor parameters resolver 58 */ 59 public FieldInitializer(Object fieldOwner, Field field, ConstructorArgumentResolver argResolver) { 60 this(fieldOwner, field, new ParameterizedConstructorInstantiator(fieldOwner, field, argResolver)); 61 } 62 63 private FieldInitializer(Object fieldOwner, Field field, ConstructorInstantiator instantiator) { 64 if(new FieldReader(fieldOwner, field).isNull()) { 65 checkNotLocal(field); 66 checkNotInner(field); 67 checkNotInterface(field); 68 checkNotAbstract(field); 69 } 70 this.fieldOwner = fieldOwner; 71 this.field = field; 72 this.instantiator = instantiator; 73 } 74 75 /** 76 * Initialize field if not initialized and return the actual instance. 77 * 78 * @return Actual field instance. 79 */ 80 public FieldInitializationReport initialize() { 81 final AccessibilityChanger changer = new AccessibilityChanger(); 82 changer.enableAccess(field); 83 84 try { 85 return acquireFieldInstance(); 86 } catch(IllegalAccessException e) { 87 throw new MockitoException("Problems initializing field '" + field.getName() + "' of type '" + field.getType().getSimpleName() + "'", e); 88 } finally { 89 changer.safelyDisableAccess(field); 90 } 91 } 92 93 private void checkNotLocal(Field field) { 94 if(field.getType().isLocalClass()) { 95 throw new MockitoException("the type '" + field.getType().getSimpleName() + "' is a local class."); 96 } 97 } 98 99 private void checkNotInner(Field field) { 100 if(field.getType().isMemberClass() && !Modifier.isStatic(field.getType().getModifiers())) { 101 throw new MockitoException("the type '" + field.getType().getSimpleName() + "' is an inner class."); 102 } 103 } 104 105 private void checkNotInterface(Field field) { 106 if(field.getType().isInterface()) { 107 throw new MockitoException("the type '" + field.getType().getSimpleName() + "' is an interface."); 108 } 109 } 110 111 private void checkNotAbstract(Field field) { 112 if(Modifier.isAbstract(field.getType().getModifiers())) { 113 throw new MockitoException("the type '" + field.getType().getSimpleName() + " is an abstract class."); 114 } 115 } 116 117 private FieldInitializationReport acquireFieldInstance() throws IllegalAccessException { 118 Object fieldInstance = field.get(fieldOwner); 119 if(fieldInstance != null) { 120 return new FieldInitializationReport(fieldInstance, false, false); 121 } 122 123 return instantiator.instantiate(); 124 } 125 126 /** 127 * Represents the strategy used to resolve actual instances 128 * to be given to a constructor given the argument types. 129 */ 130 public interface ConstructorArgumentResolver { 131 132 /** 133 * Try to resolve instances from types. 134 * 135 * <p> 136 * Checks on the real argument type or on the correct argument number 137 * will happen during the field initialization {@link FieldInitializer#initialize()}. 138 * I.e the only responsibility of this method, is to provide instances <strong>if possible</strong>. 139 * </p> 140 * 141 * @param argTypes Constructor argument types, should not be null. 142 * @return The argument instances to be given to the constructor, should not be null. 143 */ 144 Object[] resolveTypeInstances(Class<?>... argTypes); 145 } 146 147 private interface ConstructorInstantiator { 148 FieldInitializationReport instantiate(); 149 } 150 151 /** 152 * Constructor instantiating strategy for no-arg constructor. 153 * 154 * <p> 155 * If a no-arg constructor can be found then the instance is created using 156 * this constructor. 157 * Otherwise a technical MockitoException is thrown. 158 * </p> 159 */ 160 static class NoArgConstructorInstantiator implements ConstructorInstantiator { 161 private Object testClass; 162 private Field field; 163 164 /** 165 * Internal, checks are done by FieldInitializer. 166 * Fields are assumed to be accessible. 167 */ 168 NoArgConstructorInstantiator(Object testClass, Field field) { 169 this.testClass = testClass; 170 this.field = field; 171 } 172 173 public FieldInitializationReport instantiate() { 174 final AccessibilityChanger changer = new AccessibilityChanger(); 175 Constructor<?> constructor = null; 176 try { 177 constructor = field.getType().getDeclaredConstructor(); 178 changer.enableAccess(constructor); 179 180 final Object[] noArg = new Object[0]; 181 Object newFieldInstance = constructor.newInstance(noArg); 182 new FieldSetter(testClass, field).set(newFieldInstance); 183 184 return new FieldInitializationReport(field.get(testClass), true, false); 185 } catch (NoSuchMethodException e) { 186 throw new MockitoException("the type '" + field.getType().getSimpleName() + "' has no default constructor", e); 187 } catch (InvocationTargetException e) { 188 throw new MockitoException("the default constructor of type '" + field.getType().getSimpleName() + "' has raised an exception (see the stack trace for cause): " + e.getTargetException().toString(), e); 189 } catch (InstantiationException e) { 190 throw new MockitoException("InstantiationException (see the stack trace for cause): " + e.toString(), e); 191 } catch (IllegalAccessException e) { 192 throw new MockitoException("IllegalAccessException (see the stack trace for cause): " + e.toString(), e); 193 } finally { 194 if(constructor != null) { 195 changer.safelyDisableAccess(constructor); 196 } 197 } 198 } 199 } 200 201 /** 202 * Constructor instantiating strategy for parameterized constructors. 203 * 204 * <p> 205 * Choose the constructor with the highest number of parameters, then 206 * call the ConstructorArgResolver to get actual argument instances. 207 * If the argResolver fail, then a technical MockitoException is thrown is thrown. 208 * Otherwise the instance is created with the resolved arguments. 209 * </p> 210 */ 211 static class ParameterizedConstructorInstantiator implements ConstructorInstantiator { 212 private Object testClass; 213 private Field field; 214 private ConstructorArgumentResolver argResolver; 215 private Comparator<Constructor<?>> byParameterNumber = new Comparator<Constructor<?>>() { 216 public int compare(Constructor<?> constructorA, Constructor<?> constructorB) { 217 return constructorB.getParameterTypes().length - constructorA.getParameterTypes().length; 218 } 219 }; 220 221 /** 222 * Internal, checks are done by FieldInitializer. 223 * Fields are assumed to be accessible. 224 */ 225 ParameterizedConstructorInstantiator(Object testClass, Field field, ConstructorArgumentResolver argumentResolver) { 226 this.testClass = testClass; 227 this.field = field; 228 this.argResolver = argumentResolver; 229 } 230 231 public FieldInitializationReport instantiate() { 232 final AccessibilityChanger changer = new AccessibilityChanger(); 233 Constructor<?> constructor = null; 234 try { 235 constructor = biggestConstructor(field.getType()); 236 changer.enableAccess(constructor); 237 238 final Object[] args = argResolver.resolveTypeInstances(constructor.getParameterTypes()); 239 Object newFieldInstance = constructor.newInstance(args); 240 new FieldSetter(testClass, field).set(newFieldInstance); 241 242 return new FieldInitializationReport(field.get(testClass), false, true); 243 } catch (IllegalArgumentException e) { 244 throw new MockitoException("internal error : argResolver provided incorrect types for constructor " + constructor + " of type " + field.getType().getSimpleName(), e); 245 } catch (InvocationTargetException e) { 246 throw new MockitoException("the constructor of type '" + field.getType().getSimpleName() + "' has raised an exception (see the stack trace for cause): " + e.getTargetException().toString(), e); 247 } catch (InstantiationException e) { 248 throw new MockitoException("InstantiationException (see the stack trace for cause): " + e.toString(), e); 249 } catch (IllegalAccessException e) { 250 throw new MockitoException("IllegalAccessException (see the stack trace for cause): " + e.toString(), e); 251 } finally { 252 if(constructor != null) { 253 changer.safelyDisableAccess(constructor); 254 } 255 } 256 } 257 258 private void checkParameterized(Constructor<?> constructor, Field field) { 259 if(constructor.getParameterTypes().length == 0) { 260 throw new MockitoException("the field " + field.getName() + " of type " + field.getType() + " has no parameterized constructor"); 261 } 262 } 263 264 private Constructor<?> biggestConstructor(Class<?> clazz) { 265 final List<Constructor<?>> constructors = Arrays.asList(clazz.getDeclaredConstructors()); 266 Collections.sort(constructors, byParameterNumber); 267 268 Constructor<?> constructor = constructors.get(0); 269 checkParameterized(constructor, field); 270 return constructor; 271 } 272 } 273} 274