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.util;
018
019import java.lang.reflect.InvocationTargetException;
020import java.lang.reflect.Method;
021import java.lang.reflect.Modifier;
022
023/**
024 * Helper class that allows for specifying a method to invoke in a declarative
025 * fashion, be it static or non-static.
026 *
027 * <p>Usage: Specify "targetClass"/"targetMethod" or "targetObject"/"targetMethod",
028 * optionally specify arguments, prepare the invoker. Afterwards, you may
029 * invoke the method any number of times, obtaining the invocation result.
030 *
031 * @author Colin Sampaleanu
032 * @author Juergen Hoeller
033 * @since 19.02.2004
034 * @see #prepare
035 * @see #invoke
036 */
037public class MethodInvoker {
038
039        private Class<?> targetClass;
040
041        private Object targetObject;
042
043        private String targetMethod;
044
045        private String staticMethod;
046
047        private Object[] arguments;
048
049        /** The method we will call */
050        private Method methodObject;
051
052
053        /**
054         * Set the target class on which to call the target method.
055         * Only necessary when the target method is static; else,
056         * a target object needs to be specified anyway.
057         * @see #setTargetObject
058         * @see #setTargetMethod
059         */
060        public void setTargetClass(Class<?> targetClass) {
061                this.targetClass = targetClass;
062        }
063
064        /**
065         * Return the target class on which to call the target method.
066         */
067        public Class<?> getTargetClass() {
068                return this.targetClass;
069        }
070
071        /**
072         * Set the target object on which to call the target method.
073         * Only necessary when the target method is not static;
074         * else, a target class is sufficient.
075         * @see #setTargetClass
076         * @see #setTargetMethod
077         */
078        public void setTargetObject(Object targetObject) {
079                this.targetObject = targetObject;
080                if (targetObject != null) {
081                        this.targetClass = targetObject.getClass();
082                }
083        }
084
085        /**
086         * Return the target object on which to call the target method.
087         */
088        public Object getTargetObject() {
089                return this.targetObject;
090        }
091
092        /**
093         * Set the name of the method to be invoked.
094         * Refers to either a static method or a non-static method,
095         * depending on a target object being set.
096         * @see #setTargetClass
097         * @see #setTargetObject
098         */
099        public void setTargetMethod(String targetMethod) {
100                this.targetMethod = targetMethod;
101        }
102
103        /**
104         * Return the name of the method to be invoked.
105         */
106        public String getTargetMethod() {
107                return this.targetMethod;
108        }
109
110        /**
111         * Set a fully qualified static method name to invoke,
112         * e.g. "example.MyExampleClass.myExampleMethod".
113         * Convenient alternative to specifying targetClass and targetMethod.
114         * @see #setTargetClass
115         * @see #setTargetMethod
116         */
117        public void setStaticMethod(String staticMethod) {
118                this.staticMethod = staticMethod;
119        }
120
121        /**
122         * Set arguments for the method invocation. If this property is not set,
123         * or the Object array is of length 0, a method with no arguments is assumed.
124         */
125        public void setArguments(Object... arguments) {
126                this.arguments = arguments;
127        }
128
129        /**
130         * Return the arguments for the method invocation.
131         */
132        public Object[] getArguments() {
133                return (this.arguments != null ? this.arguments : new Object[0]);
134        }
135
136
137        /**
138         * Prepare the specified method.
139         * The method can be invoked any number of times afterwards.
140         * @see #getPreparedMethod
141         * @see #invoke
142         */
143        public void prepare() throws ClassNotFoundException, NoSuchMethodException {
144                if (this.staticMethod != null) {
145                        int lastDotIndex = this.staticMethod.lastIndexOf('.');
146                        if (lastDotIndex == -1 || lastDotIndex == this.staticMethod.length()) {
147                                throw new IllegalArgumentException(
148                                                "staticMethod must be a fully qualified class plus method name: " +
149                                                "e.g. 'example.MyExampleClass.myExampleMethod'");
150                        }
151                        String className = this.staticMethod.substring(0, lastDotIndex);
152                        String methodName = this.staticMethod.substring(lastDotIndex + 1);
153                        this.targetClass = resolveClassName(className);
154                        this.targetMethod = methodName;
155                }
156
157                Class<?> targetClass = getTargetClass();
158                String targetMethod = getTargetMethod();
159                Assert.notNull(targetClass, "Either 'targetClass' or 'targetObject' is required");
160                Assert.notNull(targetMethod, "Property 'targetMethod' is required");
161
162                Object[] arguments = getArguments();
163                Class<?>[] argTypes = new Class<?>[arguments.length];
164                for (int i = 0; i < arguments.length; ++i) {
165                        argTypes[i] = (arguments[i] != null ? arguments[i].getClass() : Object.class);
166                }
167
168                // Try to get the exact method first.
169                try {
170                        this.methodObject = targetClass.getMethod(targetMethod, argTypes);
171                }
172                catch (NoSuchMethodException ex) {
173                        // Just rethrow exception if we can't get any match.
174                        this.methodObject = findMatchingMethod();
175                        if (this.methodObject == null) {
176                                throw ex;
177                        }
178                }
179        }
180
181        /**
182         * Resolve the given class name into a Class.
183         * <p>The default implementations uses {@code ClassUtils.forName},
184         * using the thread context class loader.
185         * @param className the class name to resolve
186         * @return the resolved Class
187         * @throws ClassNotFoundException if the class name was invalid
188         */
189        protected Class<?> resolveClassName(String className) throws ClassNotFoundException {
190                return ClassUtils.forName(className, ClassUtils.getDefaultClassLoader());
191        }
192
193        /**
194         * Find a matching method with the specified name for the specified arguments.
195         * @return a matching method, or {@code null} if none
196         * @see #getTargetClass()
197         * @see #getTargetMethod()
198         * @see #getArguments()
199         */
200        protected Method findMatchingMethod() {
201                String targetMethod = getTargetMethod();
202                Object[] arguments = getArguments();
203                int argCount = arguments.length;
204
205                Method[] candidates = ReflectionUtils.getAllDeclaredMethods(getTargetClass());
206                int minTypeDiffWeight = Integer.MAX_VALUE;
207                Method matchingMethod = null;
208
209                for (Method candidate : candidates) {
210                        if (candidate.getName().equals(targetMethod)) {
211                                Class<?>[] paramTypes = candidate.getParameterTypes();
212                                if (paramTypes.length == argCount) {
213                                        int typeDiffWeight = getTypeDifferenceWeight(paramTypes, arguments);
214                                        if (typeDiffWeight < minTypeDiffWeight) {
215                                                minTypeDiffWeight = typeDiffWeight;
216                                                matchingMethod = candidate;
217                                        }
218                                }
219                        }
220                }
221
222                return matchingMethod;
223        }
224
225        /**
226         * Return the prepared Method object that will be invoked.
227         * <p>Can for example be used to determine the return type.
228         * @return the prepared Method object (never {@code null})
229         * @throws IllegalStateException if the invoker hasn't been prepared yet
230         * @see #prepare
231         * @see #invoke
232         */
233        public Method getPreparedMethod() throws IllegalStateException {
234                if (this.methodObject == null) {
235                        throw new IllegalStateException("prepare() must be called prior to invoke() on MethodInvoker");
236                }
237                return this.methodObject;
238        }
239
240        /**
241         * Return whether this invoker has been prepared already,
242         * i.e. whether it allows access to {@link #getPreparedMethod()} already.
243         */
244        public boolean isPrepared() {
245                return (this.methodObject != null);
246        }
247
248        /**
249         * Invoke the specified method.
250         * <p>The invoker needs to have been prepared before.
251         * @return the object (possibly null) returned by the method invocation,
252         * or {@code null} if the method has a void return type
253         * @throws InvocationTargetException if the target method threw an exception
254         * @throws IllegalAccessException if the target method couldn't be accessed
255         * @see #prepare
256         */
257        public Object invoke() throws InvocationTargetException, IllegalAccessException {
258                // In the static case, target will simply be {@code null}.
259                Object targetObject = getTargetObject();
260                Method preparedMethod = getPreparedMethod();
261                if (targetObject == null && !Modifier.isStatic(preparedMethod.getModifiers())) {
262                        throw new IllegalArgumentException("Target method must not be non-static without a target");
263                }
264                ReflectionUtils.makeAccessible(preparedMethod);
265                return preparedMethod.invoke(targetObject, getArguments());
266        }
267
268
269        /**
270         * Algorithm that judges the match between the declared parameter types of a candidate method
271         * and a specific list of arguments that this method is supposed to be invoked with.
272         * <p>Determines a weight that represents the class hierarchy difference between types and
273         * arguments. A direct match, i.e. type Integer -> arg of class Integer, does not increase
274         * the result - all direct matches means weight 0. A match between type Object and arg of
275         * class Integer would increase the weight by 2, due to the superclass 2 steps up in the
276         * hierarchy (i.e. Object) being the last one that still matches the required type Object.
277         * Type Number and class Integer would increase the weight by 1 accordingly, due to the
278         * superclass 1 step up the hierarchy (i.e. Number) still matching the required type Number.
279         * Therefore, with an arg of type Integer, a constructor (Integer) would be preferred to a
280         * constructor (Number) which would in turn be preferred to a constructor (Object).
281         * All argument weights get accumulated.
282         * <p>Note: This is the algorithm used by MethodInvoker itself and also the algorithm
283         * used for constructor and factory method selection in Spring's bean container (in case
284         * of lenient constructor resolution which is the default for regular bean definitions).
285         * @param paramTypes the parameter types to match
286         * @param args the arguments to match
287         * @return the accumulated weight for all arguments
288         */
289        public static int getTypeDifferenceWeight(Class<?>[] paramTypes, Object[] args) {
290                int result = 0;
291                for (int i = 0; i < paramTypes.length; i++) {
292                        if (!ClassUtils.isAssignableValue(paramTypes[i], args[i])) {
293                                return Integer.MAX_VALUE;
294                        }
295                        if (args[i] != null) {
296                                Class<?> paramType = paramTypes[i];
297                                Class<?> superClass = args[i].getClass().getSuperclass();
298                                while (superClass != null) {
299                                        if (paramType.equals(superClass)) {
300                                                result = result + 2;
301                                                superClass = null;
302                                        }
303                                        else if (ClassUtils.isAssignable(paramType, superClass)) {
304                                                result = result + 2;
305                                                superClass = superClass.getSuperclass();
306                                        }
307                                        else {
308                                                superClass = null;
309                                        }
310                                }
311                                if (paramType.isInterface()) {
312                                        result = result + 1;
313                                }
314                        }
315                }
316                return result;
317        }
318
319}