-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
/
ConstructorInstantiator.java
207 lines (188 loc) · 8.88 KB
/
ConstructorInstantiator.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/*
* Copyright (c) 2016 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package org.mockito.internal.creation.instance;
import static org.mockito.internal.util.StringUtil.join;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import org.mockito.creation.instance.InstantiationException;
import org.mockito.creation.instance.Instantiator;
import org.mockito.internal.configuration.plugins.Plugins;
import org.mockito.internal.util.Primitives;
import org.mockito.plugins.MemberAccessor;
public class ConstructorInstantiator implements Instantiator {
/**
* Whether or not the constructors used for creating an object refer to an outer instance or not.
* This member is only used to for constructing error messages.
* If an outer inject exists, it would be the first ([0]) element of the {@link #constructorArgs} array.
*/
private final boolean hasOuterClassInstance;
private final Object[] constructorArgs;
public ConstructorInstantiator(boolean hasOuterClassInstance, Object... constructorArgs) {
this.hasOuterClassInstance = hasOuterClassInstance;
this.constructorArgs = constructorArgs;
}
public <T> T newInstance(Class<T> cls) {
return withParams(cls, constructorArgs);
}
private <T> T withParams(Class<T> cls, Object... params) {
List<Constructor<?>> matchingConstructors = new LinkedList<Constructor<?>>();
try {
for (Constructor<?> constructor : cls.getDeclaredConstructors()) {
Class<?>[] types = constructor.getParameterTypes();
if (paramsMatch(types, params)) {
evaluateConstructor(matchingConstructors, constructor);
}
}
if (matchingConstructors.size() == 1) {
return invokeConstructor(matchingConstructors.get(0), params);
}
} catch (Exception e) {
throw paramsException(cls, e);
}
if (matchingConstructors.size() == 0) {
throw noMatchingConstructor(cls);
} else {
throw multipleMatchingConstructors(cls, matchingConstructors);
}
}
@SuppressWarnings("unchecked")
private static <T> T invokeConstructor(Constructor<?> constructor, Object... params)
throws java.lang.InstantiationException, IllegalAccessException,
InvocationTargetException {
MemberAccessor accessor = Plugins.getMemberAccessor();
return (T) accessor.newInstance(constructor, params);
}
private InstantiationException paramsException(Class<?> cls, Exception e) {
return new InstantiationException(
join(
"Unable to create instance of '" + cls.getSimpleName() + "'.",
"Please ensure the target class has "
+ constructorArgsString()
+ " and executes cleanly."),
e);
}
private String constructorArgTypes() {
int argPos = 0;
if (hasOuterClassInstance) {
++argPos;
}
String[] constructorArgTypes = new String[constructorArgs.length - argPos];
for (int i = argPos; i < constructorArgs.length; ++i) {
constructorArgTypes[i - argPos] =
constructorArgs[i] == null ? null : constructorArgs[i].getClass().getName();
}
return Arrays.toString(constructorArgTypes);
}
private InstantiationException noMatchingConstructor(Class<?> cls) {
String constructorString = constructorArgsString();
String outerInstanceHint = "";
if (hasOuterClassInstance) {
outerInstanceHint = " and provided outer instance is correct";
}
return new InstantiationException(
join(
"Unable to create instance of '" + cls.getSimpleName() + "'.",
"Please ensure that the target class has "
+ constructorString
+ outerInstanceHint
+ "."),
null);
}
private String constructorArgsString() {
String constructorString;
if (constructorArgs.length == 0 || (hasOuterClassInstance && constructorArgs.length == 1)) {
constructorString = "a 0-arg constructor";
} else {
constructorString =
"a constructor that matches these argument types: " + constructorArgTypes();
}
return constructorString;
}
private InstantiationException multipleMatchingConstructors(
Class<?> cls, List<Constructor<?>> constructors) {
return new InstantiationException(
join(
"Unable to create instance of '" + cls.getSimpleName() + "'.",
"Multiple constructors could be matched to arguments of types "
+ constructorArgTypes()
+ ":",
join("", " - ", constructors),
"If you believe that Mockito could do a better job deciding on which constructor to use, please let us know.",
"Ticket 685 contains the discussion and a workaround for ambiguous constructors using inner class.",
"See https://github.com/mockito/mockito/issues/685"),
null);
}
private static boolean paramsMatch(Class<?>[] types, Object[] params) {
if (params.length != types.length) {
return false;
}
for (int i = 0; i < params.length; i++) {
if (params[i] == null) {
if (types[i].isPrimitive()) {
return false;
}
} else if ((!types[i].isPrimitive() && !types[i].isInstance(params[i]))
|| (types[i].isPrimitive()
&& !types[i].equals(
Primitives.primitiveTypeOf(params[i].getClass())))) {
return false;
}
}
return true;
}
/**
* Evalutes {@code constructor} against the currently found {@code matchingConstructors} and determines if
* it's a better match to the given arguments, a worse match, or an equivalently good match.
* <p>
* This method tries to emulate the behavior specified in
* <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.12.2">JLS 15.12.2. Compile-Time
* Step 2: Determine Method Signature</a>. A constructor X is deemed to be a better match than constructor Y to the
* given argument list if they are both applicable, constructor X has at least one parameter than is more specific
* than the corresponding parameter of constructor Y, and constructor Y has no parameter than is more specific than
* the corresponding parameter in constructor X.
* </p>
* <p>
* If {@code constructor} is a better match than the constructors in the {@code matchingConstructors} list, the list
* is cleared, and it's added to the list as a singular best matching constructor (so far).<br/>
* If {@code constructor} is an equivalently good of a match as the constructors in the {@code matchingConstructors}
* list, it's added to the list.<br/>
* If {@code constructor} is a worse match than the constructors in the {@code matchingConstructors} list, the list
* will remain unchanged.
* </p>
*
* @param matchingConstructors A list of equivalently best matching constructors found so far
* @param constructor The constructor to be evaluated against this list
*/
private void evaluateConstructor(
List<Constructor<?>> matchingConstructors, Constructor<?> constructor) {
boolean newHasBetterParam = false;
boolean existingHasBetterParam = false;
Class<?>[] paramTypes = constructor.getParameterTypes();
for (int i = 0; i < paramTypes.length; ++i) {
Class<?> paramType = paramTypes[i];
if (!paramType.isPrimitive()) {
for (Constructor<?> existingCtor : matchingConstructors) {
Class<?> existingParamType = existingCtor.getParameterTypes()[i];
if (paramType != existingParamType) {
if (paramType.isAssignableFrom(existingParamType)) {
existingHasBetterParam = true;
} else {
newHasBetterParam = true;
}
}
}
}
}
if (!existingHasBetterParam) {
matchingConstructors.clear();
}
if (newHasBetterParam || !existingHasBetterParam) {
matchingConstructors.add(constructor);
}
}
}