blob: 688526b98844986b3dc2f5037e83b697159609c3 [file] [log] [blame]
/*
* Copyright (c) 2016 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package org.mockito.internal.creation.instance;
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.Instantiator;
import org.mockito.creation.instance.InstantiationException;
import org.mockito.internal.util.Primitives;
import org.mockito.internal.util.reflection.AccessibilityChanger;
import static org.mockito.internal.util.StringUtil.join;
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 {
AccessibilityChanger accessibility = new AccessibilityChanger();
accessibility.enableAccess(constructor);
return (T) constructor.newInstance(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);
}
}
}