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