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