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