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.Method;
020import java.lang.reflect.Modifier;
021import java.lang.reflect.Proxy;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collections;
025import java.util.Comparator;
026import java.util.HashMap;
027import java.util.LinkedHashSet;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031
032import org.springframework.core.BridgeMethodResolver;
033import org.springframework.core.MethodParameter;
034import org.springframework.core.convert.TypeDescriptor;
035import org.springframework.expression.AccessException;
036import org.springframework.expression.EvaluationContext;
037import org.springframework.expression.EvaluationException;
038import org.springframework.expression.MethodExecutor;
039import org.springframework.expression.MethodFilter;
040import org.springframework.expression.MethodResolver;
041import org.springframework.expression.TypeConverter;
042import org.springframework.expression.spel.SpelEvaluationException;
043import org.springframework.expression.spel.SpelMessage;
044
045/**
046 * Reflection-based {@link MethodResolver} used by default in {@link StandardEvaluationContext}
047 * unless explicit method resolvers have been specified.
048 *
049 * @author Andy Clement
050 * @author Juergen Hoeller
051 * @author Chris Beams
052 * @since 3.0
053 * @see StandardEvaluationContext#addMethodResolver(MethodResolver)
054 */
055public class ReflectiveMethodResolver implements MethodResolver {
056
057        // Using distance will ensure a more accurate match is discovered,
058        // more closely following the Java rules.
059        private final boolean useDistance;
060
061        private Map<Class<?>, MethodFilter> filters;
062
063
064        public ReflectiveMethodResolver() {
065                this.useDistance = true;
066        }
067
068        /**
069         * This constructor allows the ReflectiveMethodResolver to be configured such that it
070         * will use a distance computation to check which is the better of two close matches
071         * (when there are multiple matches). Using the distance computation is intended to
072         * ensure matches are more closely representative of what a Java compiler would do
073         * when taking into account boxing/unboxing and whether the method candidates are
074         * declared to handle a supertype of the type (of the argument) being passed in.
075         * @param useDistance {@code true} if distance computation should be used when
076         * calculating matches; {@code false} otherwise
077         */
078        public ReflectiveMethodResolver(boolean useDistance) {
079                this.useDistance = useDistance;
080        }
081
082
083        /**
084         * Register a filter for methods on the given type.
085         * @param type the type to filter on
086         * @param filter the corresponding method filter,
087         * or {@code null} to clear any filter for the given type
088         */
089        public void registerMethodFilter(Class<?> type, MethodFilter filter) {
090                if (this.filters == null) {
091                        this.filters = new HashMap<Class<?>, MethodFilter>();
092                }
093                if (filter != null) {
094                        this.filters.put(type, filter);
095                }
096                else {
097                        this.filters.remove(type);
098                }
099        }
100
101        /**
102         * Locate a method on a type. There are three kinds of match that might occur:
103         * <ol>
104         * <li>an exact match where the types of the arguments match the types of the constructor
105         * <li>an in-exact match where the types we are looking for are subtypes of those defined on the constructor
106         * <li>a match where we are able to convert the arguments into those expected by the constructor,
107         * according to the registered type converter
108         * </ol>
109         */
110        @Override
111        public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name,
112                        List<TypeDescriptor> argumentTypes) throws AccessException {
113
114                try {
115                        TypeConverter typeConverter = context.getTypeConverter();
116                        Class<?> type = (targetObject instanceof Class ? (Class<?>) targetObject : targetObject.getClass());
117                        List<Method> methods = new ArrayList<Method>(getMethods(type, targetObject));
118
119                        // If a filter is registered for this type, call it
120                        MethodFilter filter = (this.filters != null ? this.filters.get(type) : null);
121                        if (filter != null) {
122                                List<Method> filtered = filter.filter(methods);
123                                methods = (filtered instanceof ArrayList ? filtered : new ArrayList<Method>(filtered));
124                        }
125
126                        // Sort methods into a sensible order
127                        if (methods.size() > 1) {
128                                Collections.sort(methods, new Comparator<Method>() {
129                                        @Override
130                                        public int compare(Method m1, Method m2) {
131                                                int m1pl = m1.getParameterTypes().length;
132                                                int m2pl = m2.getParameterTypes().length;
133                                                // varargs methods go last
134                                                if (m1pl == m2pl) {
135                                                    if (!m1.isVarArgs() && m2.isVarArgs()) {
136                                                        return -1;
137                                                    }
138                                                    else if (m1.isVarArgs() && !m2.isVarArgs()) {
139                                                        return 1;
140                                                    }
141                                                    else {
142                                                        return 0;
143                                                    }
144                                                }
145                                                return (m1pl < m2pl ? -1 : (m1pl > m2pl ? 1 : 0));
146                                        }
147                                });
148                        }
149
150                        // Resolve any bridge methods
151                        for (int i = 0; i < methods.size(); i++) {
152                                methods.set(i, BridgeMethodResolver.findBridgedMethod(methods.get(i)));
153                        }
154
155                        // Remove duplicate methods (possible due to resolved bridge methods)
156                        Set<Method> methodsToIterate = new LinkedHashSet<Method>(methods);
157
158                        Method closeMatch = null;
159                        int closeMatchDistance = Integer.MAX_VALUE;
160                        Method matchRequiringConversion = null;
161                        boolean multipleOptions = false;
162
163                        for (Method method : methodsToIterate) {
164                                if (method.getName().equals(name)) {
165                                        Class<?>[] paramTypes = method.getParameterTypes();
166                                        List<TypeDescriptor> paramDescriptors = new ArrayList<TypeDescriptor>(paramTypes.length);
167                                        for (int i = 0; i < paramTypes.length; i++) {
168                                                paramDescriptors.add(new TypeDescriptor(new MethodParameter(method, i)));
169                                        }
170                                        ReflectionHelper.ArgumentsMatchInfo matchInfo = null;
171                                        if (method.isVarArgs() && argumentTypes.size() >= (paramTypes.length - 1)) {
172                                                // *sigh* complicated
173                                                matchInfo = ReflectionHelper.compareArgumentsVarargs(paramDescriptors, argumentTypes, typeConverter);
174                                        }
175                                        else if (paramTypes.length == argumentTypes.size()) {
176                                                // Name and parameter number match, check the arguments
177                                                matchInfo = ReflectionHelper.compareArguments(paramDescriptors, argumentTypes, typeConverter);
178                                        }
179                                        if (matchInfo != null) {
180                                                if (matchInfo.isExactMatch()) {
181                                                        return new ReflectiveMethodExecutor(method);
182                                                }
183                                                else if (matchInfo.isCloseMatch()) {
184                                                        if (this.useDistance) {
185                                                                int matchDistance = ReflectionHelper.getTypeDifferenceWeight(paramDescriptors, argumentTypes);
186                                                                if (closeMatch == null || matchDistance < closeMatchDistance) {
187                                                                        // This is a better match...
188                                                                        closeMatch = method;
189                                                                        closeMatchDistance = matchDistance;
190                                                                }
191                                                        }
192                                                        else {
193                                                                // Take this as a close match if there isn't one already
194                                                                if (closeMatch == null) {
195                                                                        closeMatch = method;
196                                                                }
197                                                        }
198                                                }
199                                                else if (matchInfo.isMatchRequiringConversion()) {
200                                                        if (matchRequiringConversion != null) {
201                                                                multipleOptions = true;
202                                                        }
203                                                        matchRequiringConversion = method;
204                                                }
205                                        }
206                                }
207                        }
208                        if (closeMatch != null) {
209                                return new ReflectiveMethodExecutor(closeMatch);
210                        }
211                        else if (matchRequiringConversion != null) {
212                                if (multipleOptions) {
213                                        throw new SpelEvaluationException(SpelMessage.MULTIPLE_POSSIBLE_METHODS, name);
214                                }
215                                return new ReflectiveMethodExecutor(matchRequiringConversion);
216                        }
217                        else {
218                                return null;
219                        }
220                }
221                catch (EvaluationException ex) {
222                        throw new AccessException("Failed to resolve method", ex);
223                }
224        }
225
226        private Set<Method> getMethods(Class<?> type, Object targetObject) {
227                if (targetObject instanceof Class) {
228                        Set<Method> result = new LinkedHashSet<Method>();
229                        // Add these so that static methods are invocable on the type: e.g. Float.valueOf(..)
230                        Method[] methods = getMethods(type);
231                        for (Method method : methods) {
232                                if (Modifier.isStatic(method.getModifiers())) {
233                                        result.add(method);
234                                }
235                        }
236                        // Also expose methods from java.lang.Class itself
237                        result.addAll(Arrays.asList(getMethods(Class.class)));
238                        return result;
239                }
240                else if (Proxy.isProxyClass(type)) {
241                        Set<Method> result = new LinkedHashSet<Method>();
242                        // Expose interface methods (not proxy-declared overrides) for proper vararg introspection
243                        for (Class<?> ifc : type.getInterfaces()) {
244                                Method[] methods = getMethods(ifc);
245                                for (Method method : methods) {
246                                        if (isCandidateForInvocation(method, type)) {
247                                                result.add(method);
248                                        }
249                                }
250                        }
251                        return result;
252                }
253                else {
254                        Set<Method> result = new LinkedHashSet<Method>();
255                        Method[] methods = getMethods(type);
256                        for (Method method : methods) {
257                                if (isCandidateForInvocation(method, type)) {
258                                        result.add(method);
259                                }
260                        }
261                        return result;
262                }
263        }
264
265        /**
266         * Return the set of methods for this type. The default implementation returns the
267         * result of {@link Class#getMethods()} for the given {@code type}, but subclasses
268         * may override in order to alter the results, e.g. specifying static methods
269         * declared elsewhere.
270         * @param type the class for which to return the methods
271         * @since 3.1.1
272         */
273        protected Method[] getMethods(Class<?> type) {
274                return type.getMethods();
275        }
276
277        /**
278         * Determine whether the given {@code Method} is a candidate for method resolution
279         * on an instance of the given target class.
280         * <p>The default implementation considers any method as a candidate, even for
281         * static methods sand non-user-declared methods on the {@link Object} base class.
282         * @param method the Method to evaluate
283         * @param targetClass the concrete target class that is being introspected
284         * @since 4.3.15
285         */
286        protected boolean isCandidateForInvocation(Method method, Class<?> targetClass) {
287                return true;
288        }
289
290}