/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.camel.component.jms; import javax.jms.Destination; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.MessageListener; import javax.jms.Session; import org.apache.camel.AsyncCallback; import org.apache.camel.AsyncProcessor; import org.apache.camel.Exchange; import org.apache.camel.ExchangePattern; import org.apache.camel.Processor; import org.apache.camel.RollbackExchangeException; import org.apache.camel.RuntimeCamelException; import org.apache.camel.util.AsyncProcessorConverterHelper; import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jms.core.JmsOperations; import org.springframework.jms.core.MessageCreator; import org.springframework.jms.listener.SessionAwareMessageListener; import static org.apache.camel.util.ObjectHelper.wrapRuntimeCamelException; /** * A JMS {@link MessageListener} which can be used to delegate processing to a * Camel endpoint. * * Note that instance of this object has to be thread safe (reentrant) * * @version */ public class EndpointMessageListener implements SessionAwareMessageListener { private static final Logger LOG = LoggerFactory.getLogger(EndpointMessageListener.class); private final JmsEndpoint endpoint; private final AsyncProcessor processor; private JmsBinding binding; private boolean eagerLoadingOfProperties; private Object replyToDestination; private JmsOperations template; private boolean disableReplyTo; private boolean async; public EndpointMessageListener(JmsEndpoint endpoint, Processor processor) { this.endpoint = endpoint; this.processor = AsyncProcessorConverterHelper.convert(processor); } @Override public void onMessage(Message message, Session session) throws JMSException { LOG.trace("onMessage START"); LOG.debug("{} consumer received JMS message: {}", endpoint, message); boolean sendReply; RuntimeCamelException rce; try { Object replyDestination = getReplyToDestination(message); // we can only send back a reply if there was a reply destination configured // and disableReplyTo hasn't been explicit enabled sendReply = replyDestination != null && !disableReplyTo; // we should also not send back reply to ourself if this destination and replyDestination is the same Destination destination = JmsMessageHelper.getJMSDestination(message); if (destination != null && sendReply && !endpoint.isReplyToSameDestinationAllowed() && destination.equals(replyDestination)) { LOG.debug("JMSDestination and JMSReplyTo is the same, will skip sending a reply message to itself: {}", destination); sendReply = false; } final Exchange exchange = createExchange(message, session, replyDestination); if (eagerLoadingOfProperties) { exchange.getIn().getHeaders(); } String correlationId = message.getJMSCorrelationID(); if (correlationId != null) { LOG.debug("Received Message has JMSCorrelationID [{}]", correlationId); } // process the exchange either asynchronously or synchronous LOG.trace("onMessage.process START"); AsyncCallback callback = new EndpointMessageListenerAsyncCallback(message, exchange, endpoint, sendReply, replyDestination); // async is by default false, which mean we by default will process the exchange synchronously // to keep backwards compatible, as well ensure this consumer will pickup messages in order // (eg to not consume the next message before the previous has been fully processed) // but if end user explicit configure consumerAsync=true, then we can process the message // asynchronously (unless endpoint has been configured synchronous, or we use transaction) boolean forceSync = endpoint.isSynchronous() || endpoint.isTransacted(); if (forceSync || !isAsync()) { // must process synchronous if transacted or configured to do so if (LOG.isTraceEnabled()) { LOG.trace("Processing exchange {} synchronously", exchange.getExchangeId()); } try { processor.process(exchange); } catch (Exception e) { exchange.setException(e); } finally { callback.done(true); } } else { // process asynchronous using the async routing engine if (LOG.isTraceEnabled()) { LOG.trace("Processing exchange {} asynchronously", exchange.getExchangeId()); } boolean sync = processor.process(exchange, callback); if (!sync) { // will be done async so return now return; } } // if we failed processed the exchange from the async callback task, then grab the exception rce = exchange.getException(RuntimeCamelException.class); } catch (Exception e) { rce = wrapRuntimeCamelException(e); } // an exception occurred so rethrow to trigger rollback on JMS listener // the JMS listener will use the error handler to handle the uncaught exception if (rce != null) { LOG.trace("onMessage END throwing exception: {}", rce.getMessage()); // Spring message listener container will handle uncaught exceptions // being thrown from this onMessage, and will us the ErrorHandler configured // on the JmsEndpoint to handle the exception throw rce; } LOG.trace("onMessage END"); } /** * Callback task that is performed when the exchange has been processed */ private final class EndpointMessageListenerAsyncCallback implements AsyncCallback { private final Message message; private final Exchange exchange; private final JmsEndpoint endpoint; private final boolean sendReply; private final Object replyDestination; private EndpointMessageListenerAsyncCallback(Message message, Exchange exchange, JmsEndpoint endpoint, boolean sendReply, Object replyDestination) { this.message = message; this.exchange = exchange; this.endpoint = endpoint; this.sendReply = sendReply; this.replyDestination = replyDestination; } @Override public void done(boolean doneSync) { LOG.trace("onMessage.process END"); // now we evaluate the processing of the exchange and determine if it was a success or failure // we also grab information from the exchange to be used for sending back a reply (if we are to do so) // so the following logic seems a bit complicated at first glance // if we send back a reply it can either be the message body or transferring a caused exception org.apache.camel.Message body = null; Exception cause = null; RuntimeCamelException rce = null; if (exchange.isFailed() || exchange.isRollbackOnly()) { if (exchange.isRollbackOnly()) { // rollback only so wrap an exception so we can rethrow the exception to cause rollback rce = wrapRuntimeCamelException(new RollbackExchangeException(exchange)); } else if (exchange.getException() != null) { // an exception occurred while processing if (endpoint.isTransferException()) { // send the exception as reply, so null body and set the exception as the cause body = null; cause = exchange.getException(); } else { // only throw exception if endpoint is not configured to transfer exceptions back to caller // do not send a reply but wrap and rethrow the exception rce = wrapRuntimeCamelException(exchange.getException()); } } else { org.apache.camel.Message msg = exchange.hasOut() ? exchange.getOut() : exchange.getIn(); if (msg.isFault()) { // a fault occurred while processing body = msg; cause = null; } } } else { // process OK so get the reply body if we are InOut and has a body // If the ppl don't want to send the message back, he should use the InOnly if (sendReply && exchange.getPattern().isOutCapable()) { if (exchange.hasOut()) { body = exchange.getOut(); } else { body = exchange.getIn(); } cause = null; } } // send back reply if there was no error and we are supposed to send back a reply if (rce == null && sendReply && (body != null || cause != null)) { LOG.trace("onMessage.sendReply START"); if (replyDestination instanceof Destination) { sendReply((Destination)replyDestination, message, exchange, body, cause); } else { sendReply((String)replyDestination, message, exchange, body, cause); } LOG.trace("onMessage.sendReply END"); } // if an exception occurred if (rce != null) { if (doneSync) { // we were done sync, so put exception on exchange, so we can grab it in the onMessage // method and rethrow it exchange.setException(rce); } else { // we were done async, so use the endpoint error handler if (endpoint.getErrorHandler() != null) { endpoint.getErrorHandler().handleError(rce); } } } } } public Exchange createExchange(Message message, Session session, Object replyDestination) { Exchange exchange = endpoint.createExchange(); JmsBinding binding = getBinding(); exchange.setProperty(Exchange.BINDING, binding); exchange.setIn(new JmsMessage(message, session, binding)); // lets set to an InOut if we have some kind of reply-to destination if (replyDestination != null && !disableReplyTo) { // only change pattern if not already out capable if (!exchange.getPattern().isOutCapable()) { exchange.setPattern(ExchangePattern.InOut); } } return exchange; } // Properties // ------------------------------------------------------------------------- public JmsBinding getBinding() { if (binding == null) { binding = endpoint.getBinding(); } return binding; } /** * Sets the binding used to convert from a Camel message to and from a JMS * message * * @param binding the binding to use */ public void setBinding(JmsBinding binding) { this.binding = binding; } public boolean isEagerLoadingOfProperties() { return eagerLoadingOfProperties; } public void setEagerLoadingOfProperties(boolean eagerLoadingOfProperties) { this.eagerLoadingOfProperties = eagerLoadingOfProperties; } public synchronized JmsOperations getTemplate() { if (template == null) { template = endpoint.createInOnlyTemplate(); } return template; } public void setTemplate(JmsOperations template) { this.template = template; } public boolean isDisableReplyTo() { return disableReplyTo; } /** * Allows the reply-to behaviour to be disabled */ public void setDisableReplyTo(boolean disableReplyTo) { this.disableReplyTo = disableReplyTo; } public Object getReplyToDestination() { return replyToDestination; } /** * Provides an explicit reply to destination which overrides * any incoming value of {@link Message#getJMSReplyTo()} * * @param replyToDestination the destination that should be used to send replies to * as either a String or {@link javax.jms.Destination} type. */ public void setReplyToDestination(Object replyToDestination) { this.replyToDestination = replyToDestination; } public boolean isAsync() { return async; } /** * Sets whether asynchronous routing is enabled. * <p/> * By default this is <tt>false</tt>. If configured as <tt>true</tt> then * this listener will process the {@link org.apache.camel.Exchange} asynchronous. */ public void setAsync(boolean async) { this.async = async; } // Implementation methods //------------------------------------------------------------------------- /** * Strategy to determine which correlation id to use among <tt>JMSMessageID</tt> and <tt>JMSCorrelationID</tt>. * * @param message the JMS message * @return the correlation id to use * @throws JMSException can be thrown */ protected String determineCorrelationId(final Message message) throws JMSException { final String messageId = message.getJMSMessageID(); final String correlationId = message.getJMSCorrelationID(); if (endpoint.getConfiguration().isUseMessageIDAsCorrelationID()) { return messageId; } else if (ObjectHelper.isEmpty(correlationId)) { // correlation id is empty so fallback to message id return messageId; } else { return correlationId; } } protected void sendReply(Destination replyDestination, final Message message, final Exchange exchange, final org.apache.camel.Message out, final Exception cause) { if (replyDestination == null) { LOG.debug("Cannot send reply message as there is no replyDestination for: {}", out); return; } getTemplate().send(replyDestination, new MessageCreator() { public Message createMessage(Session session) throws JMSException { Message reply = endpoint.getBinding().makeJmsMessage(exchange, out, session, cause); final String correlationID = determineCorrelationId(message); reply.setJMSCorrelationID(correlationID); if (LOG.isDebugEnabled()) { LOG.debug("{} sending reply JMS message [correlationId:{}]: {}", new Object[]{endpoint, correlationID, reply}); } return reply; } }); } protected void sendReply(String replyDestination, final Message message, final Exchange exchange, final org.apache.camel.Message out, final Exception cause) { if (replyDestination == null) { LOG.debug("Cannot send reply message as there is no replyDestination for: {}", out); return; } getTemplate().send(replyDestination, new MessageCreator() { public Message createMessage(Session session) throws JMSException { Message reply = endpoint.getBinding().makeJmsMessage(exchange, out, session, cause); final String correlationID = determineCorrelationId(message); reply.setJMSCorrelationID(correlationID); if (LOG.isDebugEnabled()) { LOG.debug("{} sending reply JMS message [correlationId:{}]: {}", new Object[]{endpoint, correlationID, reply}); } return reply; } }); } protected Object getReplyToDestination(Message message) throws JMSException { // lets send a response back if we can Object destination = getReplyToDestination(); if (destination == null) { destination = JmsMessageHelper.getJMSReplyTo(message); } return destination; } @Override public String toString() { return "EndpointMessageListener[" + endpoint + "]"; } }