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