1 /*
2 * Copyright 2002-2008 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package org.springframework.jms.listener.adapter;
18
19 import java.lang.reflect.InvocationTargetException;
20
21 import javax.jms.Destination;
22 import javax.jms.InvalidDestinationException;
23 import javax.jms.JMSException;
24 import javax.jms.Message;
25 import javax.jms.MessageListener;
26 import javax.jms.MessageProducer;
27 import javax.jms.Session;
28
29 import org.apache.commons.logging.Log;
30 import org.apache.commons.logging.LogFactory;
31
32 import org.springframework.jms.listener.SessionAwareMessageListener;
33 import org.springframework.jms.listener.SubscriptionNameProvider;
34 import org.springframework.jms.support.JmsUtils;
35 import org.springframework.jms.support.converter.MessageConversionException;
36 import org.springframework.jms.support.converter.MessageConverter;
37 import org.springframework.jms.support.converter.SimpleMessageConverter;
38 import org.springframework.jms.support.destination.DestinationResolver;
39 import org.springframework.jms.support.destination.DynamicDestinationResolver;
40 import org.springframework.util.Assert;
41 import org.springframework.util.MethodInvoker;
42 import org.springframework.util.ObjectUtils;
43
44 /**
45 * Message listener adapter that delegates the handling of messages to target
46 * listener methods via reflection, with flexible message type conversion.
47 * Allows listener methods to operate on message content types, completely
48 * independent from the JMS API.
49 *
50 * <p><b>NOTE:</b> This class requires a JMS 1.1+ provider, because it builds
51 * on the domain-independent API. <b>Use the {@link MessageListenerAdapter102
52 * MessageListenerAdapter102} subclass for JMS 1.0.2 providers.</b>
53 *
54 * <p>By default, the content of incoming JMS messages gets extracted before
55 * being passed into the target listener method, to let the target method
56 * operate on message content types such as String or byte array instead of
57 * the raw {@link Message}. Message type conversion is delegated to a Spring
58 * JMS {@link MessageConverter}. By default, a {@link SimpleMessageConverter}
59 * {@link org.springframework.jms.support.converter.SimpleMessageConverter102 (102)}
60 * will be used. (If you do not want such automatic message conversion taking
61 * place, then be sure to set the {@link #setMessageConverter MessageConverter}
62 * to <code>null</code>.)
63 *
64 * <p>If a target listener method returns a non-null object (typically of a
65 * message content type such as <code>String</code> or byte array), it will get
66 * wrapped in a JMS <code>Message</code> and sent to the response destination
67 * (either the JMS "reply-to" destination or a
68 * {@link #setDefaultResponseDestination(javax.jms.Destination) specified default
69 * destination}).
70 *
71 * <p><b>Note:</b> The sending of response messages is only available when
72 * using the {@link SessionAwareMessageListener} entry point (typically through a
73 * Spring message listener container). Usage as standard JMS {@link MessageListener}
74 * does <i>not</i> support the generation of response messages.
75 *
76 * <p>Find below some examples of method signatures compliant with this
77 * adapter class. This first example handles all <code>Message</code> types
78 * and gets passed the contents of each <code>Message</code> type as an
79 * argument. No <code>Message</code> will be sent back as all of these
80 * methods return <code>void</code>.
81 *
82 * <pre class="code">public interface MessageContentsDelegate {
83 * void handleMessage(String text);
84 * void handleMessage(Map map);
85 * void handleMessage(byte[] bytes);
86 * void handleMessage(Serializable obj);
87 * }</pre>
88 *
89 * This next example handles all <code>Message</code> types and gets
90 * passed the actual (raw) <code>Message</code> as an argument. Again, no
91 * <code>Message</code> will be sent back as all of these methods return
92 * <code>void</code>.
93 *
94 * <pre class="code">public interface RawMessageDelegate {
95 * void handleMessage(TextMessage message);
96 * void handleMessage(MapMessage message);
97 * void handleMessage(BytesMessage message);
98 * void handleMessage(ObjectMessage message);
99 * }</pre>
100 *
101 * This next example illustrates a <code>Message</code> delegate
102 * that just consumes the <code>String</code> contents of
103 * {@link javax.jms.TextMessage TextMessages}. Notice also how the
104 * name of the <code>Message</code> handling method is different from the
105 * {@link #ORIGINAL_DEFAULT_LISTENER_METHOD original} (this will have to
106 * be configured in the attandant bean definition). Again, no <code>Message</code>
107 * will be sent back as the method returns <code>void</code>.
108 *
109 * <pre class="code">public interface TextMessageContentDelegate {
110 * void onMessage(String text);
111 * }</pre>
112 *
113 * This final example illustrates a <code>Message</code> delegate
114 * that just consumes the <code>String</code> contents of
115 * {@link javax.jms.TextMessage TextMessages}. Notice how the return type
116 * of this method is <code>String</code>: This will result in the configured
117 * {@link MessageListenerAdapter} sending a {@link javax.jms.TextMessage} in response.
118 *
119 * <pre class="code">public interface ResponsiveTextMessageContentDelegate {
120 * String handleMessage(String text);
121 * }</pre>
122 *
123 * For further examples and discussion please do refer to the Spring
124 * reference documentation which describes this class (and it's attendant
125 * XML configuration) in detail.
126 *
127 * @author Juergen Hoeller
128 * @since 2.0
129 * @see #setDelegate
130 * @see #setDefaultListenerMethod
131 * @see #setDefaultResponseDestination
132 * @see #setMessageConverter
133 * @see org.springframework.jms.support.converter.SimpleMessageConverter
134 * @see org.springframework.jms.listener.SessionAwareMessageListener
135 * @see org.springframework.jms.listener.AbstractMessageListenerContainer#setMessageListener
136 */
137 public class MessageListenerAdapter implements MessageListener, SessionAwareMessageListener, SubscriptionNameProvider {
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
145 /** Logger available to subclasses */
146 protected final Log logger = LogFactory.getLog(getClass());
147
148 private Object delegate;
149
150 private String defaultListenerMethod = ORIGINAL_DEFAULT_LISTENER_METHOD;
151
152 private Object defaultResponseDestination;
153
154 private DestinationResolver destinationResolver = new DynamicDestinationResolver();
155
156 private MessageConverter messageConverter;
157
158
159 /**
160 * Create a new {@link MessageListenerAdapter} with default settings.
161 */
162 public MessageListenerAdapter() {
163 initDefaultStrategies();
164 this.delegate = this;
165 }
166
167 /**
168 * Create a new {@link MessageListenerAdapter} for the given delegate.
169 * @param delegate the delegate object
170 */
171 public MessageListenerAdapter(Object delegate) {
172 initDefaultStrategies();
173 setDelegate(delegate);
174 }
175
176
177 /**
178 * Set a target object to delegate message listening to.
179 * Specified listener methods have to be present on this target object.
180 * <p>If no explicit delegate object has been specified, listener
181 * methods are expected to present on this adapter instance, that is,
182 * on a custom subclass of this adapter, defining listener methods.
183 */
184 public void setDelegate(Object delegate) {
185 Assert.notNull(delegate, "Delegate must not be null");
186 this.delegate = delegate;
187 }
188
189 /**
190 * Return the target object to delegate message listening to.
191 */
192 protected Object getDelegate() {
193 return this.delegate;
194 }
195
196 /**
197 * Specify the name of the default listener method to delegate to,
198 * for the case where no specific listener method has been determined.
199 * Out-of-the-box value is {@link #ORIGINAL_DEFAULT_LISTENER_METHOD "handleMessage"}.
200 * @see #getListenerMethodName
201 */
202 public void setDefaultListenerMethod(String defaultListenerMethod) {
203 this.defaultListenerMethod = defaultListenerMethod;
204 }
205
206 /**
207 * Return the name of the default listener method to delegate to.
208 */
209 protected String getDefaultListenerMethod() {
210 return this.defaultListenerMethod;
211 }
212
213 /**
214 * Set the default destination to send response messages to. This will be applied
215 * in case of a request message that does not carry a "JMSReplyTo" field.
216 * <p>Response destinations are only relevant for listener methods that return
217 * result objects, which will be wrapped in a response message and sent to a
218 * response destination.
219 * <p>Alternatively, specify a "defaultResponseQueueName" or "defaultResponseTopicName",
220 * to be dynamically resolved via the DestinationResolver.
221 * @see #setDefaultResponseQueueName(String)
222 * @see #setDefaultResponseTopicName(String)
223 * @see #getResponseDestination
224 */
225 public void setDefaultResponseDestination(Destination destination) {
226 this.defaultResponseDestination = destination;
227 }
228
229 /**
230 * Set the name of the default response queue to send response messages to.
231 * This will be applied in case of a request message that does not carry a
232 * "JMSReplyTo" field.
233 * <p>Alternatively, specify a JMS Destination object as "defaultResponseDestination".
234 * @see #setDestinationResolver
235 * @see #setDefaultResponseDestination(javax.jms.Destination)
236 */
237 public void setDefaultResponseQueueName(String destinationName) {
238 this.defaultResponseDestination = new DestinationNameHolder(destinationName, false);
239 }
240
241 /**
242 * Set the name of the default response topic to send response messages to.
243 * This will be applied in case of a request message that does not carry a
244 * "JMSReplyTo" field.
245 * <p>Alternatively, specify a JMS Destination object as "defaultResponseDestination".
246 * @see #setDestinationResolver
247 * @see #setDefaultResponseDestination(javax.jms.Destination)
248 */
249 public void setDefaultResponseTopicName(String destinationName) {
250 this.defaultResponseDestination = new DestinationNameHolder(destinationName, true);
251 }
252
253 /**
254 * Set the DestinationResolver that should be used to resolve response
255 * destination names for this adapter.
256 * <p>The default resolver is a DynamicDestinationResolver. Specify a
257 * JndiDestinationResolver for resolving destination names as JNDI locations.
258 * @see org.springframework.jms.support.destination.DynamicDestinationResolver
259 * @see org.springframework.jms.support.destination.JndiDestinationResolver
260 */
261 public void setDestinationResolver(DestinationResolver destinationResolver) {
262 Assert.notNull(destinationResolver, "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 /**
297 * Standard JMS {@link MessageListener} entry point.
298 * <p>Delegates the message to the target listener method, with appropriate
299 * conversion of the message argument. In case of an exception, the
300 * {@link #handleListenerException(Throwable)} method will be invoked.
301 * <p><b>Note:</b> Does not support sending response messages based on
302 * result objects returned from listener methods. Use the
303 * {@link SessionAwareMessageListener} entry point (typically through a Spring
304 * message listener container) for handling result objects as well.
305 * @param message the incoming JMS message
306 * @see #handleListenerException
307 * @see #onMessage(javax.jms.Message, javax.jms.Session)
308 */
309 public void onMessage(Message message) {
310 try {
311 onMessage(message, null);
312 }
313 catch (Throwable ex) {
314 handleListenerException(ex);
315 }
316 }
317
318 /**
319 * Spring {@link SessionAwareMessageListener} entry point.
320 * <p>Delegates the message to the target listener method, with appropriate
321 * conversion of the message argument. If the target method returns a
322 * non-null object, wrap in a JMS message and send it back.
323 * @param message the incoming JMS message
324 * @param session the JMS session to operate on
325 * @throws JMSException if thrown by JMS API methods
326 */
327 public void onMessage(Message message, Session session) throws JMSException {
328 // Check whether the delegate is a MessageListener impl itself.
329 // In that case, the adapter will simply act as a pass-through.
330 Object delegate = getDelegate();
331 if (delegate != this) {
332 if (delegate instanceof SessionAwareMessageListener) {
333 if (session != null) {
334 ((SessionAwareMessageListener) delegate).onMessage(message, session);
335 return;
336 }
337 else if (!(delegate instanceof MessageListener)) {
338 throw new javax.jms.IllegalStateException("MessageListenerAdapter cannot handle a " +
339 "SessionAwareMessageListener delegate if it hasn't been invoked with a Session itself");
340 }
341 }
342 if (delegate instanceof MessageListener) {
343 ((MessageListener) delegate).onMessage(message);
344 return;
345 }
346 }
347
348 // Regular case: find a handler method reflectively.
349 Object convertedMessage = extractMessage(message);
350 String methodName = getListenerMethodName(message, convertedMessage);
351 if (methodName == null) {
352 throw new javax.jms.IllegalStateException("No default listener method specified: " +
353 "Either specify a non-null value for the 'defaultListenerMethod' property or " +
354 "override the 'getListenerMethodName' method.");
355 }
356
357 // Invoke the handler method with appropriate arguments.
358 Object[] listenerArguments = buildListenerArguments(convertedMessage);
359 Object result = invokeListenerMethod(methodName, listenerArguments);
360 if (result != null) {
361 handleResult(result, message, session);
362 }
363 else {
364 logger.trace("No result object given - no result to handle");
365 }
366 }
367
368 public String getSubscriptionName() {
369 if (this.delegate instanceof SubscriptionNameProvider) {
370 return ((SubscriptionNameProvider) this.delegate).getSubscriptionName();
371 }
372 else {
373 return this.delegate.getClass().getName();
374 }
375 }
376
377
378 /**
379 * Initialize the default implementations for the adapter's strategies.
380 * @see #setMessageConverter
381 * @see org.springframework.jms.support.converter.SimpleMessageConverter
382 */
383 protected void initDefaultStrategies() {
384 setMessageConverter(new SimpleMessageConverter());
385 }
386
387 /**
388 * Handle the given exception that arose during listener execution.
389 * The default implementation logs the exception at error level.
390 * <p>This method only applies when used as standard JMS {@link MessageListener}.
391 * In case of the Spring {@link SessionAwareMessageListener} mechanism,
392 * exceptions get handled by the caller instead.
393 * @param ex the exception to handle
394 * @see #onMessage(javax.jms.Message)
395 */
396 protected void handleListenerException(Throwable ex) {
397 logger.error("Listener execution failed", ex);
398 }
399
400 /**
401 * Extract the message body from the given JMS message.
402 * @param message the JMS <code>Message</code>
403 * @return the content of the message, to be passed into the
404 * listener method as argument
405 * @throws JMSException if thrown by JMS API methods
406 */
407 protected Object extractMessage(Message message) throws JMSException {
408 MessageConverter converter = getMessageConverter();
409 if (converter != null) {
410 return converter.fromMessage(message);
411 }
412 return message;
413 }
414
415 /**
416 * Determine the name of the listener method that is supposed to
417 * handle the given message.
418 * <p>The default implementation simply returns the configured
419 * default listener method, if any.
420 * @param originalMessage the JMS request message
421 * @param extractedMessage the converted JMS request message,
422 * to be passed into the listener method as argument
423 * @return the name of the listener method (never <code>null</code>)
424 * @throws JMSException if thrown by JMS API methods
425 * @see #setDefaultListenerMethod
426 */
427 protected String getListenerMethodName(Message originalMessage, Object extractedMessage) throws JMSException {
428 return getDefaultListenerMethod();
429 }
430
431 /**
432 * Build an array of arguments to be passed into the target listener method.
433 * Allows for multiple method arguments to be built from a single message object.
434 * <p>The default implementation builds an array with the given message object
435 * as sole element. This means that the extracted message will always be passed
436 * into a <i>single</i> method argument, even if it is an array, with the target
437 * method having a corresponding single argument of the array's type declared.
438 * <p>This can be overridden to treat special message content such as arrays
439 * differently, for example passing in each element of the message array
440 * as distinct method argument.
441 * @param extractedMessage the content of the message
442 * @return the array of arguments to be passed into the
443 * listener method (each element of the array corresponding
444 * to a distinct method argument)
445 */
446 protected Object[] buildListenerArguments(Object extractedMessage) {
447 return new Object[] {extractedMessage};
448 }
449
450 /**
451 * Invoke the specified listener method.
452 * @param methodName the name of the listener method
453 * @param arguments the message arguments to be passed in
454 * @return the result returned from the listener method
455 * @throws JMSException if thrown by JMS API methods
456 * @see #getListenerMethodName
457 * @see #buildListenerArguments
458 */
459 protected Object invokeListenerMethod(String methodName, Object[] arguments) throws JMSException {
460 try {
461 MethodInvoker methodInvoker = new MethodInvoker();
462 methodInvoker.setTargetObject(getDelegate());
463 methodInvoker.setTargetMethod(methodName);
464 methodInvoker.setArguments(arguments);
465 methodInvoker.prepare();
466 return methodInvoker.invoke();
467 }
468 catch (InvocationTargetException ex) {
469 Throwable targetEx = ex.getTargetException();
470 if (targetEx instanceof JMSException) {
471 throw (JMSException) targetEx;
472 }
473 else {
474 throw new ListenerExecutionFailedException(
475 "Listener method '" + methodName + "' threw exception", targetEx);
476 }
477 }
478 catch (Throwable ex) {
479 throw new ListenerExecutionFailedException("Failed to invoke target method '" + methodName +
480 "' with arguments " + ObjectUtils.nullSafeToString(arguments), ex);
481 }
482 }
483
484
485 /**
486 * Handle the given result object returned from the listener method,
487 * sending a response message back.
488 * @param result the result object to handle (never <code>null</code>)
489 * @param request the original request message
490 * @param session the JMS Session to operate on (may be <code>null</code>)
491 * @throws JMSException if thrown by JMS API methods
492 * @see #buildMessage
493 * @see #postProcessResponse
494 * @see #getResponseDestination
495 * @see #sendResponse
496 */
497 protected void handleResult(Object result, Message request, Session session) throws JMSException {
498 if (session != null) {
499 if (logger.isDebugEnabled()) {
500 logger.debug("Listener method returned result [" + result +
501 "] - generating response message for it");
502 }
503 Message response = buildMessage(session, result);
504 postProcessResponse(request, response);
505 Destination destination = getResponseDestination(request, response, session);
506 sendResponse(session, destination, response);
507 }
508 else {
509 if (logger.isWarnEnabled()) {
510 logger.warn("Listener method returned result [" + result +
511 "]: not generating response message for it because of no JMS Session given");
512 }
513 }
514 }
515
516 /**
517 * Build a JMS message to be sent as response based on the given result object.
518 * @param session the JMS Session to operate on
519 * @param result the content of the message, as returned from the listener method
520 * @return the JMS <code>Message</code> (never <code>null</code>)
521 * @throws JMSException if thrown by JMS API methods
522 * @see #setMessageConverter
523 */
524 protected Message buildMessage(Session session, Object result) throws JMSException {
525 MessageConverter converter = getMessageConverter();
526 if (converter != null) {
527 return converter.toMessage(result, session);
528 }
529 else {
530 if (!(result instanceof Message)) {
531 throw new MessageConversionException(
532 "No MessageConverter specified - cannot handle message [" + result + "]");
533 }
534 return (Message) result;
535 }
536 }
537
538 /**
539 * Post-process the given response message before it will be sent.
540 * <p>The default implementation sets the response's correlation id
541 * to the request message's correlation id, if any; otherwise to the
542 * request message id.
543 * @param request the original incoming JMS message
544 * @param response the outgoing JMS message about to be sent
545 * @throws JMSException if thrown by JMS API methods
546 * @see javax.jms.Message#setJMSCorrelationID
547 */
548 protected void postProcessResponse(Message request, Message response) throws JMSException {
549 String correlation = request.getJMSCorrelationID();
550 if (correlation == null) {
551 correlation = request.getJMSMessageID();
552 }
553 response.setJMSCorrelationID(correlation);
554 }
555
556 /**
557 * Determine a response destination for the given message.
558 * <p>The default implementation first checks the JMS Reply-To
559 * {@link Destination} of the supplied request; if that is not <code>null</code>
560 * it is returned; if it is <code>null</code>, then the configured
561 * {@link #resolveDefaultResponseDestination default response destination}
562 * is returned; if this too is <code>null</code>, then an
563 * {@link InvalidDestinationException} is thrown.
564 * @param request the original incoming JMS message
565 * @param response the outgoing JMS message about to be sent
566 * @param session the JMS Session to operate on
567 * @return the response destination (never <code>null</code>)
568 * @throws JMSException if thrown by JMS API methods
569 * @throws InvalidDestinationException if no {@link Destination} can be determined
570 * @see #setDefaultResponseDestination
571 * @see javax.jms.Message#getJMSReplyTo()
572 */
573 protected Destination getResponseDestination(Message request, Message response, Session session)
574 throws JMSException {
575
576 Destination replyTo = request.getJMSReplyTo();
577 if (replyTo == null) {
578 replyTo = resolveDefaultResponseDestination(session);
579 if (replyTo == null) {
580 throw new InvalidDestinationException("Cannot determine response destination: " +
581 "Request message does not contain reply-to destination, and no default response destination set.");
582 }
583 }
584 return replyTo;
585 }
586
587 /**
588 * Resolve the default response destination into a JMS {@link Destination}, using this
589 * accessor's {@link DestinationResolver} in case of a destination name.
590 * @return the located {@link Destination}
591 * @throws javax.jms.JMSException if resolution failed
592 * @see #setDefaultResponseDestination
593 * @see #setDefaultResponseQueueName
594 * @see #setDefaultResponseTopicName
595 * @see #setDestinationResolver
596 */
597 protected Destination resolveDefaultResponseDestination(Session session) throws JMSException {
598 if (this.defaultResponseDestination instanceof Destination) {
599 return (Destination) this.defaultResponseDestination;
600 }
601 if (this.defaultResponseDestination instanceof DestinationNameHolder) {
602 DestinationNameHolder nameHolder = (DestinationNameHolder) this.defaultResponseDestination;
603 return getDestinationResolver().resolveDestinationName(session, nameHolder.name, nameHolder.isTopic);
604 }
605 return null;
606 }
607
608 /**
609 * Send the given response message to the given destination.
610 * @param response the JMS message to send
611 * @param destination the JMS destination to send to
612 * @param session the JMS session to operate on
613 * @throws JMSException if thrown by JMS API methods
614 * @see #postProcessProducer
615 * @see javax.jms.Session#createProducer
616 * @see javax.jms.MessageProducer#send
617 */
618 protected void sendResponse(Session session, Destination destination, Message response) throws JMSException {
619 MessageProducer producer = session.createProducer(destination);
620 try {
621 postProcessProducer(producer, response);
622 producer.send(response);
623 }
624 finally {
625 JmsUtils.closeMessageProducer(producer);
626 }
627 }
628
629 /**
630 * Post-process the given message producer before using it to send the response.
631 * <p>The default implementation is empty.
632 * @param producer the JMS message producer that will be used to send the message
633 * @param response the outgoing JMS message about to be sent
634 * @throws JMSException if thrown by JMS API methods
635 */
636 protected void postProcessProducer(MessageProducer producer, Message response) throws JMSException {
637 }
638
639
640 /**
641 * Internal class combining a destination name
642 * and its target destination type (queue or topic).
643 */
644 private static class DestinationNameHolder {
645
646 public final String name;
647
648 public final boolean isTopic;
649
650 public DestinationNameHolder(String name, boolean isTopic) {
651 this.name = name;
652 this.isTopic = isTopic;
653 }
654 }
655
656 }