001/*
002 * Copyright 2006-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.batch.item.adapter;
018
019import java.lang.reflect.InvocationTargetException;
020import java.lang.reflect.Method;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.List;
024
025import org.springframework.beans.factory.InitializingBean;
026import org.springframework.util.Assert;
027import org.springframework.util.ClassUtils;
028import org.springframework.util.MethodInvoker;
029
030/**
031 * Superclass for delegating classes which dynamically call a custom method of
032 * injected object. Provides convenient API for dynamic method invocation
033 * shielding subclasses from low-level details and exception handling.
034 *
035 * {@link Exception}s thrown by a successfully invoked delegate method are
036 * re-thrown without wrapping. In case the delegate method throws a
037 * {@link Throwable} that doesn't subclass {@link Exception} it will be wrapped
038 * by {@link InvocationTargetThrowableWrapper}.
039 *
040 * @author Robert Kasanicky
041 * @author Mahmoud Ben Hassine
042 */
043public abstract class AbstractMethodInvokingDelegator<T> implements InitializingBean {
044
045        private Object targetObject;
046
047        private String targetMethod;
048
049        private Object[] arguments;
050
051        /**
052         * Invoker the target method with arguments set by
053         * {@link #setArguments(Object[])}.
054         *
055         * @return object returned by invoked method
056         *
057         * @throws Exception exception thrown when executing the delegate method.
058         */
059        protected T invokeDelegateMethod() throws Exception {
060                MethodInvoker invoker = createMethodInvoker(targetObject, targetMethod);
061                invoker.setArguments(arguments);
062                return doInvoke(invoker);
063        }
064
065        /**
066         * Invokes the target method with given argument.
067         *
068         * @param object argument for the target method
069         * @return object returned by target method
070         *
071         * @throws Exception exception thrown when executing the delegate method.
072         */
073        protected T invokeDelegateMethodWithArgument(Object object) throws Exception {
074                MethodInvoker invoker = createMethodInvoker(targetObject, targetMethod);
075                invoker.setArguments(new Object[] { object });
076                return doInvoke(invoker);
077        }
078
079        /**
080         * Invokes the target method with given arguments.
081         *
082         * @param args arguments for the invoked method
083         * @return object returned by invoked method
084         *
085         * @throws Exception exception thrown when executing the delegate method.
086         */
087        protected T invokeDelegateMethodWithArguments(Object[] args) throws Exception {
088                MethodInvoker invoker = createMethodInvoker(targetObject, targetMethod);
089                invoker.setArguments(args);
090                return doInvoke(invoker);
091        }
092
093        /**
094         * Create a new configured instance of {@link MethodInvoker}.
095         */
096        private MethodInvoker createMethodInvoker(Object targetObject, String targetMethod) {
097                HippyMethodInvoker invoker = new HippyMethodInvoker();
098                invoker.setTargetObject(targetObject);
099                invoker.setTargetMethod(targetMethod);
100                invoker.setArguments(arguments);
101                return invoker;
102        }
103
104        /**
105         * Prepare and invoke the invoker, rethrow checked exceptions as unchecked.
106         * @param invoker configured invoker
107         * @return return value of the invoked method
108         */
109        @SuppressWarnings("unchecked")
110        private T doInvoke(MethodInvoker invoker) throws Exception {
111                try {
112                        invoker.prepare();
113                }
114                catch (ClassNotFoundException | NoSuchMethodException e) {
115                        throw new DynamicMethodInvocationException(e);
116                }
117
118                try {
119                        return (T) invoker.invoke();
120                }
121                catch (InvocationTargetException e) {
122                        if (e.getCause() instanceof Exception) {
123                                throw (Exception) e.getCause();
124                        }
125                        else {
126                                throw new InvocationTargetThrowableWrapper(e.getCause());
127                        }
128                }
129                catch (IllegalAccessException e) {
130                        throw new DynamicMethodInvocationException(e);
131                }
132        }
133
134        @Override
135        public void afterPropertiesSet() throws Exception {
136                Assert.notNull(targetObject, "targetObject must not be null");
137                Assert.hasLength(targetMethod, "targetMethod must not be empty");
138                Assert.state(targetClassDeclaresTargetMethod(),
139                                "target class must declare a method with matching name and parameter types");
140        }
141
142        /**
143         * @return true if target class declares a method matching target method
144         * name with given number of arguments of appropriate type.
145         */
146        private boolean targetClassDeclaresTargetMethod() {
147                MethodInvoker invoker = createMethodInvoker(targetObject, targetMethod);
148
149                Method[] memberMethods = invoker.getTargetClass().getMethods();
150                Method[] declaredMethods = invoker.getTargetClass().getDeclaredMethods();
151
152                List<Method> allMethods = new ArrayList<>();
153                allMethods.addAll(Arrays.asList(memberMethods));
154                allMethods.addAll(Arrays.asList(declaredMethods));
155
156                String targetMethodName = invoker.getTargetMethod();
157
158                for (Method method : allMethods) {
159                        if (method.getName().equals(targetMethodName)) {
160                                Class<?>[] params = method.getParameterTypes();
161                                if (arguments == null) {
162                                        // don't check signature, assume arguments will be supplied
163                                        // correctly at runtime
164                                        return true;
165                                }
166                                if (arguments.length == params.length) {
167                                        boolean argumentsMatchParameters = true;
168                                        for (int j = 0; j < params.length; j++) {
169                                                if (arguments[j] == null) {
170                                                        continue;
171                                                }
172                                                if (!(ClassUtils.isAssignableValue(params[j], arguments[j]))) {
173                                                        argumentsMatchParameters = false;
174                                                }
175                                        }
176                                        if (argumentsMatchParameters) {
177                                                return true;
178                                        }
179                                }
180                        }
181                }
182
183                return false;
184        }
185
186        /**
187         * @param targetObject the delegate - bean id can be used to set this value
188         * in Spring configuration
189         */
190        public void setTargetObject(Object targetObject) {
191                this.targetObject = targetObject;
192        }
193
194        /**
195         * @param targetMethod name of the method to be invoked on
196         * {@link #setTargetObject(Object)}.
197         */
198        public void setTargetMethod(String targetMethod) {
199                this.targetMethod = targetMethod;
200        }
201
202        /**
203         * @param arguments arguments values for the {
204         * {@link #setTargetMethod(String)}. These will be used only when the
205         * subclass tries to invoke the target method without providing explicit
206         * argument values.
207         *
208         * If arguments are set to not-null value {@link #afterPropertiesSet()} will
209         * check the values are compatible with target method's signature. In case
210         * arguments are null (not set) method signature will not be checked and it
211         * is assumed correct values will be supplied at runtime.
212         */
213        public void setArguments(Object[] arguments) {
214                this.arguments = arguments == null ? null : Arrays.asList(arguments).toArray();
215        }
216
217        /**
218         * Return arguments.
219         * @return arguments
220         */
221        protected Object[] getArguments() {
222                return arguments;
223        }
224        
225        /**
226         * Used to wrap a {@link Throwable} (not an {@link Exception}) thrown by a
227         * reflectively-invoked delegate.
228         *
229         * @author Robert Kasanicky
230         */
231        @SuppressWarnings("serial")
232        public static class InvocationTargetThrowableWrapper extends RuntimeException {
233
234                public InvocationTargetThrowableWrapper(Throwable cause) {
235                        super(cause);
236                }
237
238        }
239}