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