001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.component.bean;
018
019import java.lang.annotation.Annotation;
020import java.lang.reflect.InvocationHandler;
021import java.lang.reflect.Method;
022import java.lang.reflect.Parameter;
023import java.lang.reflect.Type;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.List;
027import java.util.Map;
028import java.util.concurrent.Callable;
029import java.util.concurrent.ExecutionException;
030import java.util.concurrent.ExecutorService;
031import java.util.concurrent.Future;
032import java.util.concurrent.FutureTask;
033
034import org.apache.camel.Body;
035import org.apache.camel.CamelContext;
036import org.apache.camel.CamelExchangeException;
037import org.apache.camel.Endpoint;
038import org.apache.camel.Exchange;
039import org.apache.camel.ExchangePattern;
040import org.apache.camel.ExchangeProperty;
041import org.apache.camel.Header;
042import org.apache.camel.Headers;
043import org.apache.camel.InvalidPayloadException;
044import org.apache.camel.Producer;
045import org.apache.camel.RuntimeCamelException;
046import org.apache.camel.impl.DefaultExchange;
047import org.apache.camel.util.ObjectHelper;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051public abstract class AbstractCamelInvocationHandler implements InvocationHandler {
052
053    private static final Logger LOG = LoggerFactory.getLogger(CamelInvocationHandler.class);
054    private static final List<Method> EXCLUDED_METHODS = new ArrayList<>();
055    private static ExecutorService executorService;
056    protected final Endpoint endpoint;
057    protected final Producer producer;
058
059    static {
060        // exclude all java.lang.Object methods as we dont want to invoke them
061        EXCLUDED_METHODS.addAll(Arrays.asList(Object.class.getMethods()));
062    }
063
064    public AbstractCamelInvocationHandler(Endpoint endpoint, Producer producer) {
065        this.endpoint = endpoint;
066        this.producer = producer;
067    }
068
069    private static Object getBody(Exchange exchange, Class<?> type) throws InvalidPayloadException {
070        // get the body from the Exchange from either OUT or IN
071        if (exchange.hasOut()) {
072            if (exchange.getOut().getBody() != null) {
073                return exchange.getOut().getMandatoryBody(type);
074            } else {
075                return null;
076            }
077        } else {
078            if (exchange.getIn().getBody() != null) {
079                return exchange.getIn().getMandatoryBody(type);
080            } else {
081                return null;
082            }
083        }
084    }
085
086    @Override
087    public final Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
088        if (isValidMethod(method)) {
089            return doInvokeProxy(proxy, method, args);
090        } else {
091            // invalid method then invoke methods on this instead
092            if ("toString".equals(method.getName())) {
093                return this.toString();
094            } else if ("hashCode".equals(method.getName())) {
095                return this.hashCode();
096            } else if ("equals".equals(method.getName())) {
097                return Boolean.FALSE;
098            }
099            return null;
100        }
101    }
102
103    public abstract Object doInvokeProxy(Object proxy, Method method, Object[] args) throws Throwable;
104
105    @SuppressWarnings("unchecked")
106    protected Object invokeProxy(final Method method, final ExchangePattern pattern, Object[] args, boolean binding) throws Throwable {
107        final Exchange exchange = new DefaultExchange(endpoint, pattern);
108
109        //Need to check if there are mutiple arguments and the parameters have no annotations for binding,
110        //then use the original bean invocation.
111        
112        boolean canUseBinding = method.getParameterCount() == 1;
113
114        if (!canUseBinding) {
115            for (Parameter parameter : method.getParameters()) {
116                if (parameter.isAnnotationPresent(Header.class)
117                        || parameter.isAnnotationPresent(Headers.class)
118                        || parameter.isAnnotationPresent(ExchangeProperty.class)
119                        || parameter.isAnnotationPresent(Body.class)) {
120                    canUseBinding = true;
121                }
122            }
123        }
124
125        if (binding && canUseBinding) {
126            // in binding mode we bind the passed in arguments (args) to the created exchange
127            // using the existing Camel @Body, @Header, @Headers, @ExchangeProperty annotations
128            // if no annotation then its bound as the message body
129            int index = 0;
130            for (Annotation[] row : method.getParameterAnnotations()) {
131                Object value = args[index];
132                if (row == null || row.length == 0) {
133                    // assume its message body when there is no annotations
134                    exchange.getIn().setBody(value);
135                } else {
136                    for (Annotation ann : row) {
137                        if (ann.annotationType().isAssignableFrom(Header.class)) {
138                            Header header = (Header) ann;
139                            String name = header.value();
140                            exchange.getIn().setHeader(name, value);
141                        } else if (ann.annotationType().isAssignableFrom(Headers.class)) {
142                            Map map = exchange.getContext().getTypeConverter().tryConvertTo(Map.class, exchange, value);
143                            if (map != null) {
144                                exchange.getIn().getHeaders().putAll(map);
145                            }
146                        } else if (ann.annotationType().isAssignableFrom(ExchangeProperty.class)) {
147                            ExchangeProperty ep = (ExchangeProperty) ann;
148                            String name = ep.value();
149                            exchange.setProperty(name, value);
150                        } else if (ann.annotationType().isAssignableFrom(Body.class)) {
151                            exchange.getIn().setBody(value);
152                        } else {
153                            // assume its message body when there is no annotations
154                            exchange.getIn().setBody(value);
155                        }
156                    }
157                }
158                index++;
159            }
160        } else {
161            // no binding so use the old behavior with BeanInvocation as the body
162            BeanInvocation invocation = new BeanInvocation(method, args);
163            exchange.getIn().setBody(invocation);
164        }
165
166        if (binding) {
167            LOG.trace("Binding to service interface as @Body,@Header,@ExchangeProperty detected when calling proxy method: {}", method);
168        } else {
169            LOG.trace("No binding to service interface as @Body,@Header,@ExchangeProperty not detected. Using BeanInvocation as message body when calling proxy method: {}");
170        }
171
172        return doInvoke(method, exchange);
173    }
174
175    protected Object invokeWithBody(final Method method, Object body, final ExchangePattern pattern) throws Throwable {
176        final Exchange exchange = new DefaultExchange(endpoint, pattern);
177        exchange.getIn().setBody(body);
178
179        return doInvoke(method, exchange);
180    }
181
182    protected Object doInvoke(final Method method, final Exchange exchange) throws Throwable {
183
184        // is the return type a future
185        final boolean isFuture = method.getReturnType() == Future.class;
186
187        // create task to execute the proxy and gather the reply
188        FutureTask<Object> task = new FutureTask<>(new Callable<Object>() {
189            public Object call() throws Exception {
190                // process the exchange
191                LOG.trace("Proxied method call {} invoking producer: {}", method.getName(), producer);
192                producer.process(exchange);
193
194                Object answer = afterInvoke(method, exchange, exchange.getPattern(), isFuture);
195                LOG.trace("Proxied method call {} returning: {}", method.getName(), answer);
196                return answer;
197            }
198        });
199
200        if (isFuture) {
201            // submit task and return future
202            if (LOG.isTraceEnabled()) {
203                LOG.trace("Submitting task for exchange id {}", exchange.getExchangeId());
204            }
205            getExecutorService(exchange.getContext()).submit(task);
206            return task;
207        } else {
208            // execute task now
209            try {
210                task.run();
211                return task.get();
212            } catch (ExecutionException e) {
213                // we don't want the wrapped exception from JDK
214                throw e.getCause();
215            }
216        }
217    }
218
219    protected Object afterInvoke(Method method, Exchange exchange, ExchangePattern pattern, boolean isFuture) throws Exception {
220        // check if we had an exception
221        Throwable cause = exchange.getException();
222        if (cause != null) {
223            Throwable found = findSuitableException(cause, method);
224            if (found != null) {
225                if (found instanceof Exception) {
226                    throw (Exception)found;
227                } else {
228                    // wrap as exception
229                    throw new CamelExchangeException("Error processing exchange", exchange, cause);
230                }
231            }
232            // special for runtime camel exceptions as they can be nested
233            if (cause instanceof RuntimeCamelException) {
234                // if the inner cause is a runtime exception we can throw it
235                // directly
236                if (cause.getCause() instanceof RuntimeException) {
237                    throw (RuntimeException)((RuntimeCamelException)cause).getCause();
238                }
239                throw (RuntimeCamelException)cause;
240            }
241            // okay just throw the exception as is
242            if (cause instanceof Exception) {
243                throw (Exception)cause;
244            } else {
245                // wrap as exception
246                throw new CamelExchangeException("Error processing exchange", exchange, cause);
247            }
248        }
249
250        Class<?> to = isFuture ? getGenericType(exchange.getContext(), method.getGenericReturnType()) : method.getReturnType();
251
252        // do not return a reply if the method is VOID
253        if (to == Void.TYPE) {
254            return null;
255        }
256
257        return getBody(exchange, to);
258    }
259
260    protected static Class<?> getGenericType(CamelContext context, Type type) throws ClassNotFoundException {
261        if (type == null) {
262            // fallback and use object
263            return Object.class;
264        }
265
266        // unfortunately java dont provide a nice api for getting the generic
267        // type of the return type
268        // due type erasure, so we have to gather it based on a String
269        // representation
270        String name = ObjectHelper.between(type.toString(), "<", ">");
271        if (name != null) {
272            if (name.contains("<")) {
273                // we only need the outer type
274                name = ObjectHelper.before(name, "<");
275            }
276            return context.getClassResolver().resolveMandatoryClass(name);
277        } else {
278            // fallback and use object
279            return Object.class;
280        }
281    }
282
283    @SuppressWarnings("deprecation")
284    protected static synchronized ExecutorService getExecutorService(CamelContext context) {
285        // CamelContext will shutdown thread pool when it shutdown so we can
286        // lazy create it on demand
287        // but in case of hot-deploy or the likes we need to be able to
288        // re-create it (its a shared static instance)
289        if (executorService == null || executorService.isTerminated() || executorService.isShutdown()) {
290            // try to lookup a pool first based on id/profile
291            executorService = context.getExecutorServiceStrategy().lookup(CamelInvocationHandler.class, "CamelInvocationHandler", "CamelInvocationHandler");
292            if (executorService == null) {
293                executorService = context.getExecutorServiceStrategy().newDefaultThreadPool(CamelInvocationHandler.class, "CamelInvocationHandler");
294            }
295        }
296        return executorService;
297    }
298
299    /**
300     * Tries to find the best suited exception to throw.
301     * <p/>
302     * It looks in the exception hierarchy from the caused exception and matches
303     * this against the declared exceptions being thrown on the method.
304     *
305     * @param cause the caused exception
306     * @param method the method
307     * @return the exception to throw, or <tt>null</tt> if not possible to find
308     *         a suitable exception
309     */
310    protected Throwable findSuitableException(Throwable cause, Method method) {
311        if (method.getExceptionTypes() == null || method.getExceptionTypes().length == 0) {
312            return null;
313        }
314
315        // see if there is any exception which matches the declared exception on
316        // the method
317        for (Class<?> type : method.getExceptionTypes()) {
318            Object fault = ObjectHelper.getException(type, cause);
319            if (fault != null) {
320                return Throwable.class.cast(fault);
321            }
322        }
323
324        return null;
325    }
326
327    protected boolean isValidMethod(Method method) {
328        // must not be in the excluded list
329        for (Method excluded : EXCLUDED_METHODS) {
330            if (ObjectHelper.isOverridingMethod(excluded, method)) {
331                // the method is overriding an excluded method so its not valid
332                return false;
333            }
334        }
335        return true;
336    }
337
338}