001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.expression.spel.support;
018
019import java.lang.reflect.Array;
020import java.lang.reflect.Method;
021import java.util.List;
022
023import org.springframework.core.MethodParameter;
024import org.springframework.core.convert.TypeDescriptor;
025import org.springframework.expression.EvaluationException;
026import org.springframework.expression.TypeConverter;
027import org.springframework.expression.spel.SpelEvaluationException;
028import org.springframework.util.Assert;
029import org.springframework.util.ClassUtils;
030import org.springframework.util.CollectionUtils;
031import org.springframework.util.MethodInvoker;
032
033/**
034 * Utility methods used by the reflection resolver code to discover the appropriate
035 * methods/constructors and fields that should be used in expressions.
036 *
037 * @author Andy Clement
038 * @author Juergen Hoeller
039 * @since 3.0
040 */
041public class ReflectionHelper {
042
043        /**
044         * Compare argument arrays and return information about whether they match.
045         * A supplied type converter and conversionAllowed flag allow for matches to take
046         * into account that a type may be transformed into a different type by the converter.
047         * @param expectedArgTypes the types the method/constructor is expecting
048         * @param suppliedArgTypes the types that are being supplied at the point of invocation
049         * @param typeConverter a registered type converter
050         * @return a MatchInfo object indicating what kind of match it was,
051         * or {@code null} if it was not a match
052         */
053        static ArgumentsMatchInfo compareArguments(
054                        List<TypeDescriptor> expectedArgTypes, List<TypeDescriptor> suppliedArgTypes, TypeConverter typeConverter) {
055
056                Assert.isTrue(expectedArgTypes.size() == suppliedArgTypes.size(),
057                                "Expected argument types and supplied argument types should be arrays of same length");
058
059                ArgumentsMatchKind match = ArgumentsMatchKind.EXACT;
060                for (int i = 0; i < expectedArgTypes.size() && match != null; i++) {
061                        TypeDescriptor suppliedArg = suppliedArgTypes.get(i);
062                        TypeDescriptor expectedArg = expectedArgTypes.get(i);
063                        if (!expectedArg.equals(suppliedArg)) {
064                                // The user may supply null - and that will be ok unless a primitive is expected
065                                if (suppliedArg == null) {
066                                        if (expectedArg.isPrimitive()) {
067                                                match = null;
068                                        }
069                                }
070                                else {
071                                        if (suppliedArg.isAssignableTo(expectedArg)) {
072                                                if (match != ArgumentsMatchKind.REQUIRES_CONVERSION) {
073                                                        match = ArgumentsMatchKind.CLOSE;
074                                                }
075                                        }
076                                        else if (typeConverter.canConvert(suppliedArg, expectedArg)) {
077                                                match = ArgumentsMatchKind.REQUIRES_CONVERSION;
078                                        }
079                                        else {
080                                                match = null;
081                                        }
082                                }
083                        }
084                }
085                return (match != null ? new ArgumentsMatchInfo(match) : null);
086        }
087
088        /**
089         * Based on {@link MethodInvoker#getTypeDifferenceWeight(Class[], Object[])} but operates on TypeDescriptors.
090         */
091        public static int getTypeDifferenceWeight(List<TypeDescriptor> paramTypes, List<TypeDescriptor> argTypes) {
092                int result = 0;
093                for (int i = 0; i < paramTypes.size(); i++) {
094                        TypeDescriptor paramType = paramTypes.get(i);
095                        TypeDescriptor argType = (i < argTypes.size() ? argTypes.get(i) : null);
096                        if (argType == null) {
097                                if (paramType.isPrimitive()) {
098                                        return Integer.MAX_VALUE;
099                                }
100                        }
101                        else {
102                                Class<?> paramTypeClazz = paramType.getType();
103                                if (!ClassUtils.isAssignable(paramTypeClazz, argType.getType())) {
104                                        return Integer.MAX_VALUE;
105                                }
106                                if (paramTypeClazz.isPrimitive()) {
107                                        paramTypeClazz = Object.class;
108                                }
109                                Class<?> superClass = argType.getType().getSuperclass();
110                                while (superClass != null) {
111                                        if (paramTypeClazz.equals(superClass)) {
112                                                result = result + 2;
113                                                superClass = null;
114                                        }
115                                        else if (ClassUtils.isAssignable(paramTypeClazz, superClass)) {
116                                                result = result + 2;
117                                                superClass = superClass.getSuperclass();
118                                        }
119                                        else {
120                                                superClass = null;
121                                        }
122                                }
123                                if (paramTypeClazz.isInterface()) {
124                                        result = result + 1;
125                                }
126                        }
127                }
128                return result;
129        }
130
131        /**
132         * Compare argument arrays and return information about whether they match.
133         * A supplied type converter and conversionAllowed flag allow for matches to
134         * take into account that a type may be transformed into a different type by the
135         * converter. This variant of compareArguments also allows for a varargs match.
136         * @param expectedArgTypes the types the method/constructor is expecting
137         * @param suppliedArgTypes the types that are being supplied at the point of invocation
138         * @param typeConverter a registered type converter
139         * @return a MatchInfo object indicating what kind of match it was,
140         * or {@code null} if it was not a match
141         */
142        static ArgumentsMatchInfo compareArgumentsVarargs(
143                        List<TypeDescriptor> expectedArgTypes, List<TypeDescriptor> suppliedArgTypes, TypeConverter typeConverter) {
144
145                Assert.isTrue(!CollectionUtils.isEmpty(expectedArgTypes),
146                                "Expected arguments must at least include one array (the varargs parameter)");
147                Assert.isTrue(expectedArgTypes.get(expectedArgTypes.size() - 1).isArray(),
148                                "Final expected argument should be array type (the varargs parameter)");
149
150                ArgumentsMatchKind match = ArgumentsMatchKind.EXACT;
151
152                // Check up until the varargs argument:
153
154                // Deal with the arguments up to 'expected number' - 1 (that is everything but the varargs argument)
155                int argCountUpToVarargs = expectedArgTypes.size() - 1;
156                for (int i = 0; i < argCountUpToVarargs && match != null; i++) {
157                        TypeDescriptor suppliedArg = suppliedArgTypes.get(i);
158                        TypeDescriptor expectedArg = expectedArgTypes.get(i);
159                        if (suppliedArg == null) {
160                                if (expectedArg.isPrimitive()) {
161                                        match = null;
162                                }
163                        }
164                        else {
165                                if (!expectedArg.equals(suppliedArg)) {
166                                        if (suppliedArg.isAssignableTo(expectedArg)) {
167                                                if (match != ArgumentsMatchKind.REQUIRES_CONVERSION) {
168                                                        match = ArgumentsMatchKind.CLOSE;
169                                                }
170                                        }
171                                        else if (typeConverter.canConvert(suppliedArg, expectedArg)) {
172                                                match = ArgumentsMatchKind.REQUIRES_CONVERSION;
173                                        }
174                                        else {
175                                                match = null;
176                                        }
177                                }
178                        }
179                }
180
181                // If already confirmed it cannot be a match, then return
182                if (match == null) {
183                        return null;
184                }
185
186                if (suppliedArgTypes.size() == expectedArgTypes.size() &&
187                                expectedArgTypes.get(expectedArgTypes.size() - 1).equals(
188                                                suppliedArgTypes.get(suppliedArgTypes.size() - 1))) {
189                        // Special case: there is one parameter left and it is an array and it matches the varargs
190                        // expected argument - that is a match, the caller has already built the array. Proceed with it.
191                }
192                else {
193                        // Now... we have the final argument in the method we are checking as a match and we have 0
194                        // or more other arguments left to pass to it.
195                        TypeDescriptor varargsDesc = expectedArgTypes.get(expectedArgTypes.size() - 1);
196                        Class<?> varargsParamType = varargsDesc.getElementTypeDescriptor().getType();
197
198                        // All remaining parameters must be of this type or convertible to this type
199                        for (int i = expectedArgTypes.size() - 1; i < suppliedArgTypes.size(); i++) {
200                                TypeDescriptor suppliedArg = suppliedArgTypes.get(i);
201                                if (suppliedArg == null) {
202                                        if (varargsParamType.isPrimitive()) {
203                                                match = null;
204                                        }
205                                }
206                                else {
207                                        if (varargsParamType != suppliedArg.getType()) {
208                                                if (ClassUtils.isAssignable(varargsParamType, suppliedArg.getType())) {
209                                                        if (match != ArgumentsMatchKind.REQUIRES_CONVERSION) {
210                                                                match = ArgumentsMatchKind.CLOSE;
211                                                        }
212                                                }
213                                                else if (typeConverter.canConvert(suppliedArg, TypeDescriptor.valueOf(varargsParamType))) {
214                                                        match = ArgumentsMatchKind.REQUIRES_CONVERSION;
215                                                }
216                                                else {
217                                                        match = null;
218                                                }
219                                        }
220                                }
221                        }
222                }
223
224                return (match != null ? new ArgumentsMatchInfo(match) : null);
225        }
226
227
228        // TODO could do with more refactoring around argument handling and varargs
229        /**
230         * Convert a supplied set of arguments into the requested types. If the parameterTypes are related to
231         * a varargs method then the final entry in the parameterTypes array is going to be an array itself whose
232         * component type should be used as the conversion target for extraneous arguments. (For example, if the
233         * parameterTypes are {Integer, String[]} and the input arguments are {Integer, boolean, float} then both
234         * the boolean and float must be converted to strings). This method does *not* repackage the arguments
235         * into a form suitable for the varargs invocation - a subsequent call to setupArgumentsForVarargsInvocation handles that.
236         * @param converter the converter to use for type conversions
237         * @param arguments the arguments to convert to the requested parameter types
238         * @param method the target Method
239         * @return true if some kind of conversion occurred on the argument
240         * @throws SpelEvaluationException if there is a problem with conversion
241         */
242        public static boolean convertAllArguments(TypeConverter converter, Object[] arguments, Method method)
243                        throws SpelEvaluationException {
244
245                Integer varargsPosition = (method.isVarArgs() ? method.getParameterTypes().length - 1 : null);
246                return convertArguments(converter, arguments, method, varargsPosition);
247        }
248
249        /**
250         * Takes an input set of argument values and converts them to the types specified as the
251         * required parameter types. The arguments are converted 'in-place' in the input array.
252         * @param converter the type converter to use for attempting conversions
253         * @param arguments the actual arguments that need conversion
254         * @param methodOrCtor the target Method or Constructor
255         * @param varargsPosition the known position of the varargs argument, if any
256         * ({@code null} if not varargs)
257         * @return {@code true} if some kind of conversion occurred on an argument
258         * @throws EvaluationException if a problem occurs during conversion
259         */
260        static boolean convertArguments(TypeConverter converter, Object[] arguments, Object methodOrCtor,
261                        Integer varargsPosition) throws EvaluationException {
262
263                boolean conversionOccurred = false;
264                if (varargsPosition == null) {
265                        for (int i = 0; i < arguments.length; i++) {
266                                TypeDescriptor targetType = new TypeDescriptor(MethodParameter.forMethodOrConstructor(methodOrCtor, i));
267                                Object argument = arguments[i];
268                                arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType);
269                                conversionOccurred |= (argument != arguments[i]);
270                        }
271                }
272                else {
273                        // Convert everything up to the varargs position
274                        for (int i = 0; i < varargsPosition; i++) {
275                                TypeDescriptor targetType = new TypeDescriptor(MethodParameter.forMethodOrConstructor(methodOrCtor, i));
276                                Object argument = arguments[i];
277                                arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType);
278                                conversionOccurred |= (argument != arguments[i]);
279                        }
280                        MethodParameter methodParam = MethodParameter.forMethodOrConstructor(methodOrCtor, varargsPosition);
281                        if (varargsPosition == arguments.length - 1) {
282                                // If the target is varargs and there is just one more argument
283                                // then convert it here
284                                TypeDescriptor targetType = new TypeDescriptor(methodParam);
285                                Object argument = arguments[varargsPosition];
286                                TypeDescriptor sourceType = TypeDescriptor.forObject(argument);
287                                arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType);
288                                // Three outcomes of that previous line:
289                                // 1) the input argument was already compatible (ie. array of valid type) and nothing was done
290                                // 2) the input argument was correct type but not in an array so it was made into an array
291                                // 3) the input argument was the wrong type and got converted and put into an array
292                                if (argument != arguments[varargsPosition] &&
293                                                !isFirstEntryInArray(argument, arguments[varargsPosition])) {
294                                        conversionOccurred = true; // case 3
295                                }
296                        }
297                        else {
298                                // Convert remaining arguments to the varargs element type
299                                TypeDescriptor targetType = new TypeDescriptor(methodParam).getElementTypeDescriptor();
300                                for (int i = varargsPosition; i < arguments.length; i++) {
301                                        Object argument = arguments[i];
302                                        arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType);
303                                        conversionOccurred |= (argument != arguments[i]);
304                                }
305                        }
306                }
307                return conversionOccurred;
308        }
309
310        /**
311         * Check if the supplied value is the first entry in the array represented by the possibleArray value.
312         * @param value the value to check for in the array
313         * @param possibleArray an array object that may have the supplied value as the first element
314         * @return true if the supplied value is the first entry in the array
315         */
316        private static boolean isFirstEntryInArray(Object value, Object possibleArray) {
317                if (possibleArray == null) {
318                        return false;
319                }
320                Class<?> type = possibleArray.getClass();
321                if (!type.isArray() || Array.getLength(possibleArray) == 0 ||
322                                !ClassUtils.isAssignableValue(type.getComponentType(), value)) {
323                        return false;
324                }
325                Object arrayValue = Array.get(possibleArray, 0);
326                return (type.getComponentType().isPrimitive() ? arrayValue.equals(value) : arrayValue == value);
327        }
328
329        /**
330         * Package up the arguments so that they correctly match what is expected in parameterTypes.
331         * For example, if parameterTypes is {@code (int, String[])} because the second parameter
332         * was declared {@code String...}, then if arguments is {@code [1,"a","b"]} then it must be
333         * repackaged as {@code [1,new String[]{"a","b"}]} in order to match the expected types.
334         * @param requiredParameterTypes the types of the parameters for the invocation
335         * @param args the arguments to be setup ready for the invocation
336         * @return a repackaged array of arguments where any varargs setup has been done
337         */
338        public static Object[] setupArgumentsForVarargsInvocation(Class<?>[] requiredParameterTypes, Object... args) {
339                // Check if array already built for final argument
340                int parameterCount = requiredParameterTypes.length;
341                int argumentCount = args.length;
342
343                // Check if repackaging is needed...
344                if (parameterCount != args.length ||
345                                requiredParameterTypes[parameterCount - 1] !=
346                                                (args[argumentCount - 1] != null ? args[argumentCount - 1].getClass() : null)) {
347
348                        int arraySize = 0;  // zero size array if nothing to pass as the varargs parameter
349                        if (argumentCount >= parameterCount) {
350                                arraySize = argumentCount - (parameterCount - 1);
351                        }
352
353                        // Create an array for the varargs arguments
354                        Object[] newArgs = new Object[parameterCount];
355                        System.arraycopy(args, 0, newArgs, 0, newArgs.length - 1);
356
357                        // Now sort out the final argument, which is the varargs one. Before entering this method,
358                        // the arguments should have been converted to the box form of the required type.
359                        Class<?> componentType = requiredParameterTypes[parameterCount - 1].getComponentType();
360                        Object repackagedArgs = Array.newInstance(componentType, arraySize);
361                        for (int i = 0; i < arraySize; i++) {
362                                Array.set(repackagedArgs, i, args[parameterCount - 1 + i]);
363                        }
364                        newArgs[newArgs.length - 1] = repackagedArgs;
365                        return newArgs;
366                }
367                return args;
368        }
369
370
371        enum ArgumentsMatchKind {
372
373                /** An exact match is where the parameter types exactly match what the method/constructor is expecting */
374                EXACT,
375
376                /** A close match is where the parameter types either exactly match or are assignment-compatible */
377                CLOSE,
378
379                /** A conversion match is where the type converter must be used to transform some of the parameter types */
380                REQUIRES_CONVERSION
381        }
382
383
384        /**
385         * An instance of ArgumentsMatchInfo describes what kind of match was achieved
386         * between two sets of arguments - the set that a method/constructor is expecting
387         * and the set that are being supplied at the point of invocation. If the kind
388         * indicates that conversion is required for some of the arguments then the arguments
389         * that require conversion are listed in the argsRequiringConversion array.
390         */
391        static class ArgumentsMatchInfo {
392
393                private final ArgumentsMatchKind kind;
394
395                ArgumentsMatchInfo(ArgumentsMatchKind kind) {
396                        this.kind = kind;
397                }
398
399                public boolean isExactMatch() {
400                        return (this.kind == ArgumentsMatchKind.EXACT);
401                }
402
403                public boolean isCloseMatch() {
404                        return (this.kind == ArgumentsMatchKind.CLOSE);
405                }
406
407                public boolean isMatchRequiringConversion() {
408                        return (this.kind == ArgumentsMatchKind.REQUIRES_CONVERSION);
409                }
410
411                @Override
412                public String toString() {
413                        return "ArgumentMatchInfo: " + this.kind;
414                }
415        }
416
417}