• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
9 import java.lang.reflect.Constructor;
10 import java.lang.reflect.Field;
11 import java.lang.reflect.InvocationTargetException;
12 import java.lang.reflect.Modifier;
13 import java.util.Arrays;
14 import java.util.Collections;
15 import java.util.Comparator;
16 import 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  */
27 public 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      */
FieldInitializer(Object fieldOwner, Field field)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      */
FieldInitializer(Object fieldOwner, Field field, ConstructorArgumentResolver argResolver)59     public FieldInitializer(Object fieldOwner, Field field, ConstructorArgumentResolver argResolver) {
60         this(fieldOwner, field, new ParameterizedConstructorInstantiator(fieldOwner, field, argResolver));
61     }
62 
FieldInitializer(Object fieldOwner, Field field, ConstructorInstantiator instantiator)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      */
initialize()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 
checkNotLocal(Field field)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 
checkNotInner(Field field)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 
checkNotInterface(Field field)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 
checkNotAbstract(Field field)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 
acquireFieldInstance()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          */
resolveTypeInstances(Class<?>.... argTypes)144         Object[] resolveTypeInstances(Class<?>... argTypes);
145     }
146 
147     private interface ConstructorInstantiator {
instantiate()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          */
NoArgConstructorInstantiator(Object testClass, Field field)168         NoArgConstructorInstantiator(Object testClass, Field field) {
169             this.testClass = testClass;
170             this.field = field;
171         }
172 
instantiate()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          */
ParameterizedConstructorInstantiator(Object testClass, Field field, ConstructorArgumentResolver argumentResolver)225         ParameterizedConstructorInstantiator(Object testClass, Field field, ConstructorArgumentResolver argumentResolver) {
226             this.testClass = testClass;
227             this.field = field;
228             this.argResolver = argumentResolver;
229         }
230 
instantiate()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 
checkParameterized(Constructor<?> constructor, Field field)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 
biggestConstructor(Class<?> clazz)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