001: /*
002: * Copyright 2002-2007 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: * http://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:
017: package org.springframework.jms.listener.adapter;
018:
019: import java.lang.reflect.InvocationTargetException;
020:
021: import javax.jms.Destination;
022: import javax.jms.InvalidDestinationException;
023: import javax.jms.JMSException;
024: import javax.jms.Message;
025: import javax.jms.MessageListener;
026: import javax.jms.MessageProducer;
027: import javax.jms.Session;
028:
029: import org.apache.commons.logging.Log;
030: import org.apache.commons.logging.LogFactory;
031:
032: import org.springframework.jms.listener.SessionAwareMessageListener;
033: import org.springframework.jms.support.JmsUtils;
034: import org.springframework.jms.support.converter.MessageConversionException;
035: import org.springframework.jms.support.converter.MessageConverter;
036: import org.springframework.jms.support.converter.SimpleMessageConverter;
037: import org.springframework.jms.support.destination.DestinationResolver;
038: import org.springframework.jms.support.destination.DynamicDestinationResolver;
039: import org.springframework.util.Assert;
040: import org.springframework.util.MethodInvoker;
041: import org.springframework.util.ObjectUtils;
042:
043: /**
044: * Message listener adapter that delegates the handling of messages to target
045: * listener methods via reflection, with flexible message type conversion.
046: * Allows listener methods to operate on message content types, completely
047: * independent from the JMS API.
048: *
049: * <p>By default, the content of incoming JMS messages gets extracted before
050: * being passed into the target listener method, to let the target method
051: * operate on message content types such as String or byte array instead of
052: * the raw {@link Message}. Message type conversion is delegated to a Spring
053: * JMS {@link MessageConverter}. By default, a {@link SimpleMessageConverter}
054: * {@link org.springframework.jms.support.converter.SimpleMessageConverter102 (102)}
055: * will be used. (If you do not want such automatic message conversion taking
056: * place, then be sure to set the {@link #setMessageConverter MessageConverter}
057: * to <code>null</code>.)
058: *
059: * <p>If a target listener method returns a non-null object (typically of a
060: * message content type such as <code>String</code> or byte array), it will get
061: * wrapped in a JMS <code>Message</code> and sent to the response destination
062: * (either the JMS "reply-to" destination or a
063: * {@link #setDefaultResponseDestination(javax.jms.Destination) specified default
064: * destination}).
065: *
066: * <p><b>Note:</b> The sending of response messages is only available when
067: * using the {@link SessionAwareMessageListener} entry point (typically through a
068: * Spring message listener container). Usage as standard JMS {@link MessageListener}
069: * does <i>not</i> support the generation of response messages.
070: *
071: * <p>This class requires a JMS 1.1+ provider, because it builds on the
072: * domain-independent API. <b>Use the {@link MessageListenerAdapter102
073: * MessageListenerAdapter102} subclass for JMS 1.0.2 providers.</b>
074: *
075: * <p>Find below some examples of method signatures compliant with this
076: * adapter class. This first example handles all <code>Message</code> types
077: * and gets passed the contents of each <code>Message</code> type as an
078: * argument. No <code>Message</code> will be sent back as all of these
079: * methods return <code>void</code>.
080: *
081: * <pre class="code">public interface MessageContentsDelegate {
082: * void handleMessage(String text);
083: * void handleMessage(Map map);
084: * void handleMessage(byte[] bytes);
085: * void handleMessage(Serializable obj);
086: * }</pre>
087: *
088: * This next example handles all <code>Message</code> types and gets
089: * passed the actual (raw) <code>Message</code> as an argument. Again, no
090: * <code>Message</code> will be sent back as all of these methods return
091: * <code>void</code>.
092: *
093: * <pre class="code">public interface RawMessageDelegate {
094: * void handleMessage(TextMessage message);
095: * void handleMessage(MapMessage message);
096: * void handleMessage(BytesMessage message);
097: * void handleMessage(ObjectMessage message);
098: * }</pre>
099: *
100: * This next example illustrates a <code>Message</code> delegate
101: * that just consumes the <code>String</code> contents of
102: * {@link javax.jms.TextMessage TextMessages}. Notice also how the
103: * name of the <code>Message</code> handling method is different from the
104: * {@link #ORIGINAL_DEFAULT_LISTENER_METHOD original} (this will have to
105: * be configured in the attandant bean definition). Again, no <code>Message</code>
106: * will be sent back as the method returns <code>void</code>.
107: *
108: * <pre class="code">public interface TextMessageContentDelegate {
109: * void onMessage(String text);
110: * }</pre>
111: *
112: * This final example illustrates a <code>Message</code> delegate
113: * that just consumes the <code>String</code> contents of
114: * {@link javax.jms.TextMessage TextMessages}. Notice how the return type
115: * of this method is <code>String</code>: This will result in the configured
116: * {@link MessageListenerAdapter} sending a {@link javax.jms.TextMessage} in response.
117: *
118: * <pre class="code">public interface ResponsiveTextMessageContentDelegate {
119: * String handleMessage(String text);
120: * }</pre>
121: *
122: * For further examples and discussion please do refer to the Spring
123: * reference documentation which describes this class (and it's attendant
124: * XML configuration) in detail.
125: *
126: * @author Juergen Hoeller
127: * @since 2.0
128: * @see #setDelegate
129: * @see #setDefaultListenerMethod
130: * @see #setDefaultResponseDestination
131: * @see #setMessageConverter
132: * @see org.springframework.jms.support.converter.SimpleMessageConverter
133: * @see org.springframework.jms.listener.SessionAwareMessageListener
134: * @see org.springframework.jms.listener.AbstractMessageListenerContainer#setMessageListener
135: */
136: public class MessageListenerAdapter implements MessageListener,
137: SessionAwareMessageListener {
138:
139: /**
140: * Out-of-the-box value for the default listener method: "handleMessage".
141: */
142: public static final String ORIGINAL_DEFAULT_LISTENER_METHOD = "handleMessage";
143:
144: /** Logger available to subclasses */
145: protected final Log logger = LogFactory.getLog(getClass());
146:
147: private Object delegate;
148:
149: private String defaultListenerMethod = ORIGINAL_DEFAULT_LISTENER_METHOD;
150:
151: private Object defaultResponseDestination;
152:
153: private DestinationResolver destinationResolver = new DynamicDestinationResolver();
154:
155: private MessageConverter messageConverter;
156:
157: /**
158: * Create a new {@link MessageListenerAdapter} with default settings.
159: */
160: public MessageListenerAdapter() {
161: initDefaultStrategies();
162: this .delegate = this ;
163: }
164:
165: /**
166: * Create a new {@link MessageListenerAdapter} for the given delegate.
167: */
168: public MessageListenerAdapter(Object delegate) {
169: initDefaultStrategies();
170: setDelegate(delegate);
171: }
172:
173: /**
174: * Set a target object to delegate message listening to.
175: * Specified listener methods have to be present on this target object.
176: * <p>If no explicit delegate object has been specified, listener
177: * methods are expected to present on this adapter instance, that is,
178: * on a custom subclass of this adapter, defining listener methods.
179: */
180: public void setDelegate(Object delegate) {
181: Assert.notNull(delegate, "Delegate must not be null");
182: this .delegate = delegate;
183: }
184:
185: /**
186: * Return the target object to delegate message listening to.
187: */
188: protected Object getDelegate() {
189: return this .delegate;
190: }
191:
192: /**
193: * Specify the name of the default listener method to delegate to,
194: * for the case where no specific listener method has been determined.
195: * Out-of-the-box value is {@link #ORIGINAL_DEFAULT_LISTENER_METHOD "handleMessage"}.
196: * @see #getListenerMethodName
197: */
198: public void setDefaultListenerMethod(String defaultListenerMethod) {
199: this .defaultListenerMethod = defaultListenerMethod;
200: }
201:
202: /**
203: * Return the name of the default listener method to delegate to.
204: */
205: protected String getDefaultListenerMethod() {
206: return this .defaultListenerMethod;
207: }
208:
209: /**
210: * Set the default destination to send response messages to. This will be applied
211: * in case of a request message that does not carry a "JMSReplyTo" field.
212: * <p>Response destinations are only relevant for listener methods that return
213: * result objects, which will be wrapped in a response message and sent to a
214: * response destination.
215: * <p>Alternatively, specify a "defaultResponseQueueName" or "defaultResponseTopicName",
216: * to be dynamically resolved via the DestinationResolver.
217: * @see #setDefaultResponseQueueName(String)
218: * @see #setDefaultResponseTopicName(String)
219: * @see #getResponseDestination
220: */
221: public void setDefaultResponseDestination(Destination destination) {
222: this .defaultResponseDestination = destination;
223: }
224:
225: /**
226: * Set the name of the default response queue to send response messages to.
227: * This will be applied in case of a request message that does not carry a
228: * "JMSReplyTo" field.
229: * <p>Alternatively, specify a JMS Destination object as "defaultResponseDestination".
230: * @see #setDestinationResolver
231: * @see #setDefaultResponseDestination(javax.jms.Destination)
232: */
233: public void setDefaultResponseQueueName(String destinationName) {
234: this .defaultResponseDestination = new DestinationNameHolder(
235: destinationName, false);
236: }
237:
238: /**
239: * Set the name of the default response topic to send response messages to.
240: * This will be applied in case of a request message that does not carry a
241: * "JMSReplyTo" field.
242: * <p>Alternatively, specify a JMS Destination object as "defaultResponseDestination".
243: * @see #setDestinationResolver
244: * @see #setDefaultResponseDestination(javax.jms.Destination)
245: */
246: public void setDefaultResponseTopicName(String destinationName) {
247: this .defaultResponseDestination = new DestinationNameHolder(
248: destinationName, true);
249: }
250:
251: /**
252: * Set the DestinationResolver that should be used to resolve response
253: * destination names for this adapter.
254: * <p>The default resolver is a DynamicDestinationResolver. Specify a
255: * JndiDestinationResolver for resolving destination names as JNDI locations.
256: * @see org.springframework.jms.support.destination.DynamicDestinationResolver
257: * @see org.springframework.jms.support.destination.JndiDestinationResolver
258: */
259: public void setDestinationResolver(
260: DestinationResolver destinationResolver) {
261: Assert.notNull(destinationResolver,
262: "DestinationResolver must not be null");
263: this .destinationResolver = destinationResolver;
264: }
265:
266: /**
267: * Return the DestinationResolver for this adapter.
268: */
269: protected DestinationResolver getDestinationResolver() {
270: return this .destinationResolver;
271: }
272:
273: /**
274: * Set the converter that will convert incoming JMS messages to
275: * listener method arguments, and objects returned from listener
276: * methods back to JMS messages.
277: * <p>The default converter is a {@link SimpleMessageConverter}, which is able
278: * to handle {@link javax.jms.BytesMessage BytesMessages},
279: * {@link javax.jms.TextMessage TextMessages} and
280: * {@link javax.jms.ObjectMessage ObjectMessages}.
281: */
282: public void setMessageConverter(MessageConverter messageConverter) {
283: this .messageConverter = messageConverter;
284: }
285:
286: /**
287: * Return the converter that will convert incoming JMS messages to
288: * listener method arguments, and objects returned from listener
289: * methods back to JMS messages.
290: */
291: protected MessageConverter getMessageConverter() {
292: return this .messageConverter;
293: }
294:
295: /**
296: * Standard JMS {@link MessageListener} entry point.
297: * <p>Delegates the message to the target listener method, with appropriate
298: * conversion of the message argument. In case of an exception, the
299: * {@link #handleListenerException(Throwable)} method will be invoked.
300: * <p><b>Note:</b> Does not support sending response messages based on
301: * result objects returned from listener methods. Use the
302: * {@link SessionAwareMessageListener} entry point (typically through a Spring
303: * message listener container) for handling result objects as well.
304: * @param message the incoming JMS message
305: * @see #handleListenerException
306: * @see #onMessage(javax.jms.Message, javax.jms.Session)
307: */
308: public void onMessage(Message message) {
309: try {
310: onMessage(message, null);
311: } catch (Throwable ex) {
312: handleListenerException(ex);
313: }
314: }
315:
316: /**
317: * Spring {@link SessionAwareMessageListener} entry point.
318: * <p>Delegates the message to the target listener method, with appropriate
319: * conversion of the message argument. If the target method returns a
320: * non-null object, wrap in a JMS message and send it back.
321: * @param message the incoming JMS message
322: * @param session the JMS session to operate on
323: * @throws JMSException if thrown by JMS API methods
324: */
325: public void onMessage(Message message, Session session)
326: throws JMSException {
327: Object convertedMessage = extractMessage(message);
328: String methodName = getListenerMethodName(message,
329: convertedMessage);
330:
331: if (methodName == null) {
332: Object delegate = getDelegate();
333: if (delegate != this ) {
334: if (delegate instanceof SessionAwareMessageListener) {
335: if (session != null) {
336: ((SessionAwareMessageListener) delegate)
337: .onMessage(message, session);
338: return;
339: } else if (!(delegate instanceof MessageListener)) {
340: throw new javax.jms.IllegalStateException(
341: "MessageListenerAdapter cannot handle a "
342: + "SessionAwareMessageListener delegate if it hasn't been invoked with a Session itself");
343: }
344: }
345: if (delegate instanceof MessageListener) {
346: ((MessageListener) delegate).onMessage(message);
347: return;
348: }
349: }
350: throw new javax.jms.IllegalStateException(
351: "No default listener method specified: "
352: + "Either specify a non-null value for the 'defaultListenerMethod' property or "
353: + "override the 'getListenerMethodName' method.");
354: }
355:
356: Object[] listenerArguments = buildListenerArguments(convertedMessage);
357: Object result = invokeListenerMethod(methodName,
358: listenerArguments);
359: if (result != null) {
360: handleResult(result, message, session);
361: } else {
362: logger
363: .debug("No result object given - no result to handle");
364: }
365: }
366:
367: /**
368: * Initialize the default implementations for the adapter's strategies.
369: * @see #setMessageConverter
370: * @see org.springframework.jms.support.converter.SimpleMessageConverter
371: */
372: protected void initDefaultStrategies() {
373: setMessageConverter(new SimpleMessageConverter());
374: }
375:
376: /**
377: * Handle the given exception that arose during listener execution.
378: * The default implementation logs the exception at error level.
379: * <p>This method only applies when used as standard JMS {@link MessageListener}.
380: * In case of the Spring {@link SessionAwareMessageListener} mechanism,
381: * exceptions get handled by the caller instead.
382: * @param ex the exception to handle
383: * @see #onMessage(javax.jms.Message)
384: */
385: protected void handleListenerException(Throwable ex) {
386: logger.error("Listener execution failed", ex);
387: }
388:
389: /**
390: * Extract the message body from the given JMS message.
391: * @param message the JMS <code>Message</code>
392: * @return the content of the message, to be passed into the
393: * listener method as argument
394: * @throws JMSException if thrown by JMS API methods
395: */
396: protected Object extractMessage(Message message)
397: throws JMSException {
398: MessageConverter converter = getMessageConverter();
399: if (converter != null) {
400: return converter.fromMessage(message);
401: }
402: return message;
403: }
404:
405: /**
406: * Determine the name of the listener method that is supposed to
407: * handle the given message.
408: * <p>The default implementation simply returns the configured
409: * default listener method, if any.
410: * @param originalMessage the JMS request message
411: * @param extractedMessage the converted JMS request message,
412: * to be passed into the listener method as argument
413: * @return the name of the listener method (never <code>null</code>)
414: * @throws JMSException if thrown by JMS API methods
415: * @see #setDefaultListenerMethod
416: */
417: protected String getListenerMethodName(Message originalMessage,
418: Object extractedMessage) throws JMSException {
419: return getDefaultListenerMethod();
420: }
421:
422: /**
423: * Build an array of arguments to be passed into the target listener method.
424: * Allows for multiple method arguments to be built from a single message object.
425: * <p>The default implementation builds an array with the given message object
426: * as sole element. This means that the extracted message will always be passed
427: * into a <i>single</i> method argument, even if it is an array, with the target
428: * method having a corresponding single argument of the array's type declared.
429: * <p>This can be overridden to treat special message content such as arrays
430: * differently, for example passing in each element of the message array
431: * as distinct method argument.
432: * @param extractedMessage the content of the message
433: * @return the array of arguments to be passed into the
434: * listener method (each element of the array corresponding
435: * to a distinct method argument)
436: */
437: protected Object[] buildListenerArguments(Object extractedMessage) {
438: return new Object[] { extractedMessage };
439: }
440:
441: /**
442: * Invoke the specified listener method.
443: * @param methodName the name of the listener method
444: * @param arguments the message arguments to be passed in
445: * @return the result returned from the listener method
446: * @throws JMSException if thrown by JMS API methods
447: * @see #getListenerMethodName
448: * @see #buildListenerArguments
449: */
450: protected Object invokeListenerMethod(String methodName,
451: Object[] arguments) throws JMSException {
452: try {
453: MethodInvoker methodInvoker = new MethodInvoker();
454: methodInvoker.setTargetObject(getDelegate());
455: methodInvoker.setTargetMethod(methodName);
456: methodInvoker.setArguments(arguments);
457: methodInvoker.prepare();
458: return methodInvoker.invoke();
459: } catch (InvocationTargetException ex) {
460: throw new ListenerExecutionFailedException(
461: "Listener method '" + methodName
462: + "' threw exception", ex
463: .getTargetException());
464: } catch (Throwable ex) {
465: throw new ListenerExecutionFailedException(
466: "Failed to invoke target method '" + methodName
467: + "' with arguments "
468: + ObjectUtils.nullSafeToString(arguments),
469: ex);
470: }
471: }
472:
473: /**
474: * Handle the given result object returned from the listener method,
475: * sending a response message back.
476: * @param result the result object to handle (never <code>null</code>)
477: * @param request the original request message
478: * @param session the JMS Session to operate on (may be <code>null</code>)
479: * @throws JMSException if thrown by JMS API methods
480: * @see #buildMessage
481: * @see #postProcessResponse
482: * @see #getResponseDestination
483: * @see #sendResponse
484: */
485: protected void handleResult(Object result, Message request,
486: Session session) throws JMSException {
487: if (session != null) {
488: if (logger.isDebugEnabled()) {
489: logger.debug("Listener method returned result ["
490: + result
491: + "] - generating response message for it");
492: }
493: Message response = buildMessage(session, result);
494: postProcessResponse(request, response);
495: Destination destination = getResponseDestination(request,
496: response, session);
497: sendResponse(session, destination, response);
498: } else {
499: if (logger.isDebugEnabled()) {
500: logger
501: .debug("Listener method returned result ["
502: + result
503: + "]: not generating response message for it because of no JMS Session given");
504: }
505: }
506: }
507:
508: /**
509: * Build a JMS message to be sent as response based on the given result object.
510: * @param session the JMS Session to operate on
511: * @param result the content of the message, as returned from the listener method
512: * @return the JMS <code>Message</code> (never <code>null</code>)
513: * @throws JMSException if thrown by JMS API methods
514: * @see #setMessageConverter
515: */
516: protected Message buildMessage(Session session, Object result)
517: throws JMSException {
518: MessageConverter converter = getMessageConverter();
519: if (converter != null) {
520: return converter.toMessage(result, session);
521: } else {
522: if (!(result instanceof Message)) {
523: throw new MessageConversionException(
524: "No MessageConverter specified - cannot handle message ["
525: + result + "]");
526: }
527: return (Message) result;
528: }
529: }
530:
531: /**
532: * Post-process the given response message before it will be sent.
533: * <p>The default implementation sets the response's correlation id
534: * to the request message's correlation id.
535: * @param request the original incoming JMS message
536: * @param response the outgoing JMS message about to be sent
537: * @throws JMSException if thrown by JMS API methods
538: * @see javax.jms.Message#setJMSCorrelationID
539: */
540: protected void postProcessResponse(Message request, Message response)
541: throws JMSException {
542: response.setJMSCorrelationID(request.getJMSCorrelationID());
543: }
544:
545: /**
546: * Determine a response destination for the given message.
547: * <p>The default implementation first checks the JMS Reply-To
548: * {@link Destination} of the supplied request; if that is not <code>null</code>
549: * it is returned; if it is <code>null</code>, then the configured
550: * {@link #resolveDefaultResponseDestination default response destination}
551: * is returned; if this too is <code>null</code>, then an
552: * {@link InvalidDestinationException} is thrown.
553: * @param request the original incoming JMS message
554: * @param response the outgoing JMS message about to be sent
555: * @param session the JMS Session to operate on
556: * @return the response destination (never <code>null</code>)
557: * @throws JMSException if thrown by JMS API methods
558: * @throws InvalidDestinationException if no {@link Destination} can be determined
559: * @see #setDefaultResponseDestination
560: * @see javax.jms.Message#getJMSReplyTo()
561: */
562: protected Destination getResponseDestination(Message request,
563: Message response, Session session) throws JMSException {
564:
565: Destination replyTo = request.getJMSReplyTo();
566: if (replyTo == null) {
567: replyTo = resolveDefaultResponseDestination(session);
568: if (replyTo == null) {
569: throw new InvalidDestinationException(
570: "Cannot determine response destination: "
571: + "Request message does not contain reply-to destination, and no default response destination set.");
572: }
573: }
574: return replyTo;
575: }
576:
577: /**
578: * Resolve the default response destination into a JMS {@link Destination}, using this
579: * accessor's {@link DestinationResolver} in case of a destination name.
580: * @return the located {@link Destination}
581: * @throws javax.jms.JMSException if resolution failed
582: * @see #setDefaultResponseDestination
583: * @see #setDefaultResponseQueueName
584: * @see #setDefaultResponseTopicName
585: * @see #setDestinationResolver
586: */
587: protected Destination resolveDefaultResponseDestination(
588: Session session) throws JMSException {
589: if (this .defaultResponseDestination instanceof Destination) {
590: return (Destination) this .defaultResponseDestination;
591: }
592: if (this .defaultResponseDestination instanceof DestinationNameHolder) {
593: DestinationNameHolder nameHolder = (DestinationNameHolder) this .defaultResponseDestination;
594: return getDestinationResolver().resolveDestinationName(
595: session, nameHolder.name, nameHolder.isTopic);
596: }
597: return null;
598: }
599:
600: /**
601: * Send the given response message to the given destination.
602: * @param response the JMS message to send
603: * @param destination the JMS destination to send to
604: * @param session the JMS session to operate on
605: * @throws JMSException if thrown by JMS API methods
606: * @see #postProcessProducer
607: * @see javax.jms.Session#createProducer
608: * @see javax.jms.MessageProducer#send
609: */
610: protected void sendResponse(Session session,
611: Destination destination, Message response)
612: throws JMSException {
613: MessageProducer producer = session.createProducer(destination);
614: try {
615: postProcessProducer(producer, response);
616: producer.send(response);
617: } finally {
618: JmsUtils.closeMessageProducer(producer);
619: }
620: }
621:
622: /**
623: * Post-process the given message producer before using it to send the response.
624: * <p>The default implementation is empty.
625: * @param producer the JMS message producer that will be used to send the message
626: * @param response the outgoing JMS message about to be sent
627: * @throws JMSException if thrown by JMS API methods
628: */
629: protected void postProcessProducer(MessageProducer producer,
630: Message response) throws JMSException {
631: }
632:
633: /**
634: * Internal class combining a destination name
635: * and its target destination type (queue or topic).
636: */
637: private static class DestinationNameHolder {
638:
639: public final String name;
640:
641: public final boolean isTopic;
642:
643: public DestinationNameHolder(String name, boolean isTopic) {
644: this.name = name;
645: this.isTopic = isTopic;
646: }
647: }
648:
649: }
|