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