/*
* 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);
}
}
}