View Javadoc
1   /*
2    * Copyright 2002-2015 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.expression.spel.support;
18  
19  import java.lang.reflect.Array;
20  import java.lang.reflect.Method;
21  import java.util.List;
22  
23  import org.springframework.core.MethodParameter;
24  import org.springframework.core.convert.TypeDescriptor;
25  import org.springframework.expression.EvaluationException;
26  import org.springframework.expression.TypeConverter;
27  import org.springframework.expression.spel.SpelEvaluationException;
28  import org.springframework.util.Assert;
29  import org.springframework.util.ClassUtils;
30  import org.springframework.util.MethodInvoker;
31  
32  /**
33   * Utility methods used by the reflection resolver code to discover the appropriate
34   * methods/constructors and fields that should be used in expressions.
35   *
36   * @author Andy Clement
37   * @author Juergen Hoeller
38   * @since 3.0
39   */
40  public class ReflectionHelper {
41  
42  	/**
43  	 * Compare argument arrays and return information about whether they match.
44  	 * A supplied type converter and conversionAllowed flag allow for matches to take
45  	 * into account that a type may be transformed into a different type by the converter.
46  	 * @param expectedArgTypes the types the method/constructor is expecting
47  	 * @param suppliedArgTypes the types that are being supplied at the point of invocation
48  	 * @param typeConverter a registered type converter
49  	 * @return a MatchInfo object indicating what kind of match it was,
50  	 * or {@code null} if it was not a match
51  	 */
52  	static ArgumentsMatchInfo compareArguments(
53  			List<TypeDescriptor> expectedArgTypes, List<TypeDescriptor> suppliedArgTypes, TypeConverter typeConverter) {
54  
55  		Assert.isTrue(expectedArgTypes.size() == suppliedArgTypes.size(),
56  				"Expected argument types and supplied argument types should be arrays of same length");
57  
58  		ArgumentsMatchKind match = ArgumentsMatchKind.EXACT;
59  		for (int i = 0; i < expectedArgTypes.size() && match != null; i++) {
60  			TypeDescriptor suppliedArg = suppliedArgTypes.get(i);
61  			TypeDescriptor expectedArg = expectedArgTypes.get(i);
62  			if (!expectedArg.equals(suppliedArg)) {
63  				// The user may supply null - and that will be ok unless a primitive is expected
64  				if (suppliedArg == null) {
65  					if (expectedArg.isPrimitive()) {
66  						match = null;
67  					}
68  				}
69  				else {
70  					if (suppliedArg.isAssignableTo(expectedArg)) {
71  						if (match != ArgumentsMatchKind.REQUIRES_CONVERSION) {
72  							match = ArgumentsMatchKind.CLOSE;
73  						}
74  					}
75  					else if (typeConverter.canConvert(suppliedArg, expectedArg)) {
76  						match = ArgumentsMatchKind.REQUIRES_CONVERSION;
77  					}
78  					else {
79  						match = null;
80  					}
81  				}
82  			}
83  		}
84  		return (match != null ? new ArgumentsMatchInfo(match) : null);
85  	}
86  
87  	/**
88  	 * Based on {@link MethodInvoker#getTypeDifferenceWeight(Class[], Object[])} but operates on TypeDescriptors.
89  	 */
90  	public static int getTypeDifferenceWeight(List<TypeDescriptor> paramTypes, List<TypeDescriptor> argTypes) {
91  		int result = 0;
92  		for (int i = 0; i < paramTypes.size(); i++) {
93  			TypeDescriptor paramType = paramTypes.get(i);
94  			TypeDescriptor argType = (i < argTypes.size() ? argTypes.get(i) : null);
95  			if (argType == null) {
96  				if (paramType.isPrimitive()) {
97  					return Integer.MAX_VALUE;
98  				}
99  			}
100 			else {
101 				Class<?> paramTypeClazz = paramType.getType();
102 				if (!ClassUtils.isAssignable(paramTypeClazz, argType.getType())) {
103 					return Integer.MAX_VALUE;
104 				}
105 				if (paramTypeClazz.isPrimitive()) {
106 					paramTypeClazz = Object.class;
107 				}
108 				Class<?> superClass = argType.getType().getSuperclass();
109 				while (superClass != null) {
110 					if (paramTypeClazz.equals(superClass)) {
111 						result = result + 2;
112 						superClass = null;
113 					}
114 					else if (ClassUtils.isAssignable(paramTypeClazz, superClass)) {
115 						result = result + 2;
116 						superClass = superClass.getSuperclass();
117 					}
118 					else {
119 						superClass = null;
120 					}
121 				}
122 				if (paramTypeClazz.isInterface()) {
123 					result = result + 1;
124 				}
125 			}
126 		}
127 		return result;
128 	}
129 
130 	/**
131 	 * Compare argument arrays and return information about whether they match.
132 	 * A supplied type converter and conversionAllowed flag allow for matches to
133 	 * take into account that a type may be transformed into a different type by the
134 	 * converter. This variant of compareArguments also allows for a varargs match.
135 	 * @param expectedArgTypes the types the method/constructor is expecting
136 	 * @param suppliedArgTypes the types that are being supplied at the point of invocation
137 	 * @param typeConverter a registered type converter
138 	 * @return a MatchInfo object indicating what kind of match it was,
139 	 * or {@code null} if it was not a match
140 	 */
141 	static ArgumentsMatchInfo compareArgumentsVarargs(
142 			List<TypeDescriptor> expectedArgTypes, List<TypeDescriptor> suppliedArgTypes, TypeConverter typeConverter) {
143 
144 		Assert.isTrue(expectedArgTypes != null && expectedArgTypes.size() > 0,
145 				"Expected arguments must at least include one array (the vargargs parameter)");
146 		Assert.isTrue(expectedArgTypes.get(expectedArgTypes.size() - 1).isArray(),
147 				"Final expected argument should be array type (the varargs parameter)");
148 
149 		ArgumentsMatchKind match = ArgumentsMatchKind.EXACT;
150 
151 		// Check up until the varargs argument:
152 
153 		// Deal with the arguments up to 'expected number' - 1 (that is everything but the varargs argument)
154 		int argCountUpToVarargs = expectedArgTypes.size() - 1;
155 		for (int i = 0; i < argCountUpToVarargs && match != null; i++) {
156 			TypeDescriptor suppliedArg = suppliedArgTypes.get(i);
157 			TypeDescriptor expectedArg = expectedArgTypes.get(i);
158 			if (suppliedArg == null) {
159 				if (expectedArg.isPrimitive()) {
160 					match = null;
161 				}
162 			}
163 			else {
164 				if (!expectedArg.equals(suppliedArg)) {
165 					if (suppliedArg.isAssignableTo(expectedArg)) {
166 						if (match != ArgumentsMatchKind.REQUIRES_CONVERSION) {
167 							match = ArgumentsMatchKind.CLOSE;
168 						}
169 					}
170 					else if (typeConverter.canConvert(suppliedArg, expectedArg)) {
171 						match = ArgumentsMatchKind.REQUIRES_CONVERSION;
172 					}
173 					else {
174 						match = null;
175 					}
176 				}
177 			}
178 		}
179 
180 		// If already confirmed it cannot be a match, then return
181 		if (match == null) {
182 			return null;
183 		}
184 
185 		if (suppliedArgTypes.size() == expectedArgTypes.size() &&
186 				expectedArgTypes.get(expectedArgTypes.size() - 1).equals(
187 						suppliedArgTypes.get(suppliedArgTypes.size() - 1))) {
188 			// Special case: there is one parameter left and it is an array and it matches the varargs
189 			// expected argument - that is a match, the caller has already built the array. Proceed with it.
190 		}
191 		else {
192 			// Now... we have the final argument in the method we are checking as a match and we have 0
193 			// or more other arguments left to pass to it.
194 			TypeDescriptor varargsDesc = expectedArgTypes.get(expectedArgTypes.size() - 1);
195 			Class<?> varargsParamType = varargsDesc.getElementTypeDescriptor().getType();
196 
197 			// All remaining parameters must be of this type or convertable to this type
198 			for (int i = expectedArgTypes.size() - 1; i < suppliedArgTypes.size(); i++) {
199 				TypeDescriptor suppliedArg = suppliedArgTypes.get(i);
200 				if (suppliedArg == null) {
201 					if (varargsParamType.isPrimitive()) {
202 						match = null;
203 					}
204 				}
205 				else {
206 					if (varargsParamType != suppliedArg.getType()) {
207 						if (ClassUtils.isAssignable(varargsParamType, suppliedArg.getType())) {
208 							if (match != ArgumentsMatchKind.REQUIRES_CONVERSION) {
209 								match = ArgumentsMatchKind.CLOSE;
210 							}
211 						}
212 						else if (typeConverter.canConvert(suppliedArg, TypeDescriptor.valueOf(varargsParamType))) {
213 							match = ArgumentsMatchKind.REQUIRES_CONVERSION;
214 						}
215 						else {
216 							match = null;
217 						}
218 					}
219 				}
220 			}
221 		}
222 
223 		return (match != null ? new ArgumentsMatchInfo(match) : null);
224 	}
225 
226 
227 	// TODO could do with more refactoring around argument handling and varargs
228 	/**
229 	 * Convert a supplied set of arguments into the requested types. If the parameterTypes are related to
230 	 * a varargs method then the final entry in the parameterTypes array is going to be an array itself whose
231 	 * component type should be used as the conversion target for extraneous arguments. (For example, if the
232 	 * parameterTypes are {Integer, String[]} and the input arguments are {Integer, boolean, float} then both
233 	 * the boolean and float must be converted to strings). This method does *not* repackage the arguments
234 	 * into a form suitable for the varargs invocation - a subsequent call to setupArgumentsForVarargsInvocation handles that.
235 	 * @param converter the converter to use for type conversions
236 	 * @param arguments the arguments to convert to the requested parameter types
237 	 * @param method the target Method
238 	 * @return true if some kind of conversion occurred on the argument
239 	 * @throws SpelEvaluationException if there is a problem with conversion
240 	 */
241 	public static boolean convertAllArguments(TypeConverter converter, Object[] arguments, Method method) throws SpelEvaluationException {
242 		Integer varargsPosition = method.isVarArgs() ? method.getParameterTypes().length-1:null;
243 		return convertArguments(converter, arguments, method, varargsPosition);
244 	}
245 	
246 	/**
247 	 * Takes an input set of argument values and converts them to the types specified as the
248 	 * required parameter types. The arguments are converted 'in-place' in the input array.
249 	 * @param converter the type converter to use for attempting conversions
250 	 * @param arguments the actual arguments that need conversion
251 	 * @param methodOrCtor the target Method or Constructor
252 	 * @param varargsPosition the known position of the varargs argument, if any
253 	 * ({@code null} if not varargs)
254 	 * @return {@code true} if some kind of conversion occurred on an argument
255 	 * @throws EvaluationException if a problem occurs during conversion
256 	 */
257 	static boolean convertArguments(TypeConverter converter, Object[] arguments, Object methodOrCtor,
258 			Integer varargsPosition) throws EvaluationException {
259 
260 		boolean conversionOccurred = false;
261 		if (varargsPosition == null) {
262 			for (int i = 0; i < arguments.length; i++) {
263 				TypeDescriptor targetType = new TypeDescriptor(MethodParameter.forMethodOrConstructor(methodOrCtor, i));
264 				Object argument = arguments[i];
265 				arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType);
266 				conversionOccurred |= (argument != arguments[i]);
267 			}
268 		}
269 		else {
270 			// Convert everything up to the varargs position
271 			for (int i = 0; i < varargsPosition; i++) {
272 				TypeDescriptor targetType = new TypeDescriptor(MethodParameter.forMethodOrConstructor(methodOrCtor, i));
273 				Object argument = arguments[i];
274 				arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType);
275 				conversionOccurred |= (argument != arguments[i]);
276 			}
277 			MethodParameter methodParam = MethodParameter.forMethodOrConstructor(methodOrCtor, varargsPosition);
278 			if (varargsPosition == arguments.length - 1) {
279 				// If the target is varargs and there is just one more argument
280 				// then convert it here
281 				TypeDescriptor targetType = new TypeDescriptor(methodParam);
282 				Object argument = arguments[varargsPosition];
283 				TypeDescriptor sourceType = TypeDescriptor.forObject(argument);
284 				arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType);
285 				// Three outcomes of that previous line:
286 				// 1) the input argument was already compatible (ie. array of valid type) and nothing was done
287 				// 2) the input argument was correct type but not in an array so it was made into an array
288 				// 3) the input argument was the wrong type and got converted and put into an array
289 				if (argument != arguments[varargsPosition] &&
290 						!isFirstEntryInArray(argument, arguments[varargsPosition])) {
291 					conversionOccurred = true; // case 3
292 				}
293 			}
294 			else {
295 				// Convert remaining arguments to the varargs element type
296 				TypeDescriptor targetType = new TypeDescriptor(methodParam).getElementTypeDescriptor();
297 				for (int i = varargsPosition; i < arguments.length; i++) {
298 					Object argument = arguments[i];
299 					arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType);
300 					conversionOccurred |= (argument != arguments[i]);
301 				}
302 			}
303 		}
304 		return conversionOccurred;
305 	}
306 
307 	/**
308 	 * Check if the supplied value is the first entry in the array represented by the possibleArray value.
309 	 * @param value the value to check for in the array
310 	 * @param possibleArray an array object that may have the supplied value as the first element
311 	 * @return true if the supplied value is the first entry in the array
312 	 */
313 	private static boolean isFirstEntryInArray(Object value, Object possibleArray) {
314 		if (possibleArray == null) {
315 			return false;
316 		}
317 		Class<?> type = possibleArray.getClass();
318 		if (!type.isArray() || Array.getLength(possibleArray) == 0 ||
319 				!ClassUtils.isAssignableValue(type.getComponentType(), value)) {
320 			return false;
321 		}
322 		Object arrayValue = Array.get(possibleArray, 0);
323 		return (type.getComponentType().isPrimitive() ? arrayValue.equals(value) : arrayValue == value);
324 	}
325 
326 	/**
327 	 * Package up the arguments so that they correctly match what is expected in parameterTypes.
328 	 * For example, if parameterTypes is {@code (int, String[])} because the second parameter
329 	 * was declared {@code String...}, then if arguments is {@code [1,"a","b"]} then it must be
330 	 * repackaged as {@code [1,new String[]{"a","b"}]} in order to match the expected types.
331 	 * @param requiredParameterTypes the types of the parameters for the invocation
332 	 * @param args the arguments to be setup ready for the invocation
333 	 * @return a repackaged array of arguments where any varargs setup has been done
334 	 */
335 	public static Object[] setupArgumentsForVarargsInvocation(Class<?>[] requiredParameterTypes, Object... args) {
336 		// Check if array already built for final argument
337 		int parameterCount = requiredParameterTypes.length;
338 		int argumentCount = args.length;
339 
340 		// Check if repackaging is needed...
341 		if (parameterCount != args.length ||
342 				requiredParameterTypes[parameterCount - 1] !=
343 						(args[argumentCount - 1] != null ? args[argumentCount - 1].getClass() : null)) {
344 
345 			int arraySize = 0;  // zero size array if nothing to pass as the varargs parameter
346 			if (argumentCount >= parameterCount) {
347 				arraySize = argumentCount - (parameterCount - 1);
348 			}
349 
350 			// Create an array for the varargs arguments
351 			Object[] newArgs = new Object[parameterCount];
352 			System.arraycopy(args, 0, newArgs, 0, newArgs.length - 1);
353 
354 			// Now sort out the final argument, which is the varargs one. Before entering this method,
355 			// the arguments should have been converted to the box form of the required type.
356 			Class<?> componentType = requiredParameterTypes[parameterCount - 1].getComponentType();
357 			Object repackagedArgs = Array.newInstance(componentType, arraySize);
358 			for (int i = 0; i < arraySize; i++) {
359 				Array.set(repackagedArgs, i, args[parameterCount - 1 + i]);
360 			}
361 			newArgs[newArgs.length - 1] = repackagedArgs;
362 			return newArgs;
363 		}
364 		return args;
365 	}
366 
367 
368 	static enum ArgumentsMatchKind {
369 
370 		/** An exact match is where the parameter types exactly match what the method/constructor is expecting */
371 		EXACT,
372 
373 		/** A close match is where the parameter types either exactly match or are assignment-compatible */
374 		CLOSE,
375 
376 		/** A conversion match is where the type converter must be used to transform some of the parameter types */
377 		REQUIRES_CONVERSION
378 	}
379 
380 
381 	/**
382 	 * An instance of ArgumentsMatchInfo describes what kind of match was achieved
383 	 * between two sets of arguments - the set that a method/constructor is expecting
384 	 * and the set that are being supplied at the point of invocation. If the kind
385 	 * indicates that conversion is required for some of the arguments then the arguments
386 	 * that require conversion are listed in the argsRequiringConversion array.
387 	 */
388 	static class ArgumentsMatchInfo {
389 
390 		private final ArgumentsMatchKind kind;
391 
392 		ArgumentsMatchInfo(ArgumentsMatchKind kind) {
393 			this.kind = kind;
394 		}
395 
396 		public boolean isExactMatch() {
397 			return (this.kind == ArgumentsMatchKind.EXACT);
398 		}
399 
400 		public boolean isCloseMatch() {
401 			return (this.kind == ArgumentsMatchKind.CLOSE);
402 		}
403 
404 		public boolean isMatchRequiringConversion() {
405 			return (this.kind == ArgumentsMatchKind.REQUIRES_CONVERSION);
406 		}
407 
408 		@Override
409 		public String toString() {
410 			return "ArgumentMatchInfo: " + this.kind;
411 		}
412 	}
413 
414 }