001/*
002 * Copyright 2002-2008 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 */
016package org.springframework.batch.support;
017
018import java.lang.annotation.Annotation;
019import java.lang.annotation.ElementType;
020import java.lang.annotation.Target;
021import java.lang.reflect.Method;
022import java.util.concurrent.atomic.AtomicReference;
023
024import org.springframework.aop.framework.Advised;
025import org.springframework.core.annotation.AnnotationUtils;
026import org.springframework.util.Assert;
027import org.springframework.util.ClassUtils;
028import org.springframework.util.ObjectUtils;
029import org.springframework.util.ReflectionUtils;
030
031/**
032 * Utility methods for create MethodInvoker instances.
033 * 
034 * @author Lucas Ward
035 * @since 2.0
036 */
037public class MethodInvokerUtils {
038
039        /**
040         * Create a {@link MethodInvoker} using the provided method name to search.
041         * 
042         * @param object to be invoked
043         * @param methodName of the method to be invoked
044         * @param paramsRequired boolean indicating whether the parameters are
045         * required, if false, a no args version of the method will be searched for.
046         * @param paramTypes - parameter types of the method to search for.
047         * @return MethodInvoker if the method is found, null if it is not.
048         */
049        public static MethodInvoker getMethodInvokerByName(Object object, String methodName, boolean paramsRequired,
050                        Class<?>... paramTypes) {
051                Assert.notNull(object, "Object to invoke must not be null");
052                Method method = ClassUtils.getMethodIfAvailable(object.getClass(), methodName, paramTypes);
053                if (method == null) {
054                        String errorMsg = "no method found with name [" + methodName + "] on class ["
055                                        + object.getClass().getSimpleName() + "] compatible with the signature ["
056                                        + getParamTypesString(paramTypes) + "].";
057                        Assert.isTrue(!paramsRequired, errorMsg);
058                        // if no method was found for the given parameters, and the
059                        // parameters aren't required, then try with no params
060                        method = ClassUtils.getMethodIfAvailable(object.getClass(), methodName);
061                        Assert.notNull(method, errorMsg);
062                }
063                return new SimpleMethodInvoker(object, method);
064        }
065
066        /**
067         * Create a String representation of the array of parameter types.
068         * 
069         * @param paramTypes types of the parameters to be used
070         * @return String a String representation of those types
071         */
072        public static String getParamTypesString(Class<?>... paramTypes) {
073                StringBuilder paramTypesList = new StringBuilder("(");
074                for (int i = 0; i < paramTypes.length; i++) {
075                        paramTypesList.append(paramTypes[i].getSimpleName());
076                        if (i + 1 < paramTypes.length) {
077                                paramTypesList.append(", ");
078                        }
079                }
080                return paramTypesList.append(")").toString();
081        }
082
083        /**
084         * Create a {@link MethodInvoker} using the provided interface, and method
085         * name from that interface.
086         * 
087         * @param cls the interface to search for the method named
088         * @param methodName of the method to be invoked
089         * @param object to be invoked
090         * @param paramTypes - parameter types of the method to search for.
091         * @return MethodInvoker if the method is found, null if it is not.
092         */
093        public static MethodInvoker getMethodInvokerForInterface(Class<?> cls, String methodName, Object object,
094                        Class<?>... paramTypes) {
095
096                if (cls.isAssignableFrom(object.getClass())) {
097                        return MethodInvokerUtils.getMethodInvokerByName(object, methodName, true, paramTypes);
098                }
099                else {
100                        return null;
101                }
102        }
103
104        /**
105         * Create a MethodInvoker from the delegate based on the annotationType.
106         * Ensure that the annotated method has a valid set of parameters.
107         * 
108         * @param annotationType the annotation to scan for
109         * @param target the target object
110         * @param expectedParamTypes the expected parameter types for the method
111         * @return a MethodInvoker
112         */
113        public static MethodInvoker getMethodInvokerByAnnotation(final Class<? extends Annotation> annotationType,
114                        final Object target, final Class<?>... expectedParamTypes) {
115                MethodInvoker mi = MethodInvokerUtils.getMethodInvokerByAnnotation(annotationType, target);
116                final Class<?> targetClass = (target instanceof Advised) ? ((Advised) target).getTargetSource()
117                                .getTargetClass() : target.getClass();
118                if (mi != null) {
119                        ReflectionUtils.doWithMethods(targetClass, method -> {
120                                Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType);
121                                if (annotation != null) {
122                                        Class<?>[] paramTypes = method.getParameterTypes();
123                                        if (paramTypes.length > 0) {
124                                                String errorMsg = "The method [" + method.getName() + "] on target class ["
125                                                                + targetClass.getSimpleName() + "] is incompatible with the signature ["
126                                                                + getParamTypesString(expectedParamTypes) + "] expected for the annotation ["
127                                                                + annotationType.getSimpleName() + "].";
128
129                                                Assert.isTrue(paramTypes.length == expectedParamTypes.length, errorMsg);
130                                                for (int i = 0; i < paramTypes.length; i++) {
131                                                        Assert.isTrue(expectedParamTypes[i].isAssignableFrom(paramTypes[i]), errorMsg);
132                                                }
133                                        }
134                                }
135                        });
136                }
137                return mi;
138        }
139
140        /**
141         * Create {@link MethodInvoker} for the method with the provided annotation
142         * on the provided object. Annotations that cannot be applied to methods
143         * (i.e. that aren't annotated with an element type of METHOD) will cause an
144         * exception to be thrown.
145         * 
146         * @param annotationType to be searched for
147         * @param target to be invoked
148         * @return MethodInvoker for the provided annotation, null if none is found.
149         */
150        public static MethodInvoker getMethodInvokerByAnnotation(final Class<? extends Annotation> annotationType,
151                        final Object target) {
152                Assert.notNull(target, "Target must not be null");
153                Assert.notNull(annotationType, "AnnotationType must not be null");
154                Assert.isTrue(ObjectUtils.containsElement(annotationType.getAnnotation(Target.class).value(),
155                                ElementType.METHOD), "Annotation [" + annotationType + "] is not a Method-level annotation.");
156                final Class<?> targetClass = (target instanceof Advised) ? ((Advised) target).getTargetSource()
157                                .getTargetClass() : target.getClass();
158                if (targetClass == null) {
159                        // Proxy with no target cannot have annotations
160                        return null;
161                }
162                final AtomicReference<Method> annotatedMethod = new AtomicReference<>();
163                ReflectionUtils.doWithMethods(targetClass, method -> {
164                        Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType);
165                        if (annotation != null) {
166                                Assert.isNull(annotatedMethod.get(), "found more than one method on target class ["
167                                                + targetClass.getSimpleName() + "] with the annotation type ["
168                                                + annotationType.getSimpleName() + "].");
169                                annotatedMethod.set(method);
170                        }
171                });
172                Method method = annotatedMethod.get();
173                if (method == null) {
174                        return null;
175                }
176                else {
177                        return new SimpleMethodInvoker(target, annotatedMethod.get());
178                }
179        }
180
181        /**
182         * Create a {@link MethodInvoker} for the delegate from a single public
183         * method.
184         * 
185         * @param target an object to search for an appropriate method.
186         * @param <C> the class.
187         * @param <T> the type.
188         * @return a {@link MethodInvoker} that calls a method on the delegate.
189         */
190        public static <C, T> MethodInvoker getMethodInvokerForSingleArgument(Object target) {
191                final AtomicReference<Method> methodHolder = new AtomicReference<>();
192                ReflectionUtils.doWithMethods(target.getClass(), method -> {
193                        if (method.getParameterTypes() == null || method.getParameterTypes().length != 1) {
194                                return;
195                        }
196                        if (method.getReturnType().equals(Void.TYPE) || ReflectionUtils.isEqualsMethod(method)) {
197                                return;
198                        }
199                        Assert.state(methodHolder.get() == null,
200                                        "More than one non-void public method detected with single argument.");
201                        methodHolder.set(method);
202                });
203                Method method = methodHolder.get();
204                return new SimpleMethodInvoker(target, method);
205        }
206}