001/*
002 * Copyright 2002-2016 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 *      https://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
017package org.springframework.jms.listener.adapter;
018
019import java.lang.reflect.InvocationTargetException;
020import javax.jms.JMSException;
021import javax.jms.Message;
022import javax.jms.MessageListener;
023import javax.jms.Session;
024
025import org.springframework.jms.listener.SessionAwareMessageListener;
026import org.springframework.jms.listener.SubscriptionNameProvider;
027import org.springframework.jms.support.converter.MessageConverter;
028import org.springframework.jms.support.converter.SimpleMessageConverter;
029import org.springframework.util.Assert;
030import org.springframework.util.MethodInvoker;
031import org.springframework.util.ObjectUtils;
032
033/**
034 * Message listener adapter that delegates the handling of messages to target
035 * listener methods via reflection, with flexible message type conversion.
036 * Allows listener methods to operate on message content types, completely
037 * independent from the JMS API.
038 *
039 * <p>By default, the content of incoming JMS messages gets extracted before
040 * being passed into the target listener method, to let the target method
041 * operate on message content types such as String or byte array instead of
042 * the raw {@link Message}. Message type conversion is delegated to a Spring
043 * JMS {@link MessageConverter}. By default, a {@link SimpleMessageConverter}
044 * will be used. (If you do not want such automatic message conversion taking
045 * place, then be sure to set the {@link #setMessageConverter MessageConverter}
046 * to {@code null}.)
047 *
048 * <p>If a target listener method returns a non-null object (typically of a
049 * message content type such as {@code String} or byte array), it will get
050 * wrapped in a JMS {@code Message} and sent to the response destination
051 * (either the JMS "reply-to" destination or a
052 * {@link #setDefaultResponseDestination(javax.jms.Destination) specified default
053 * destination}).
054 *
055 * <p><b>Note:</b> The sending of response messages is only available when
056 * using the {@link SessionAwareMessageListener} entry point (typically through a
057 * Spring message listener container). Usage as standard JMS {@link MessageListener}
058 * does <i>not</i> support the generation of response messages.
059 *
060 * <p>Find below some examples of method signatures compliant with this
061 * adapter class. This first example handles all {@code Message} types
062 * and gets passed the contents of each {@code Message} type as an
063 * argument. No {@code Message} will be sent back as all of these
064 * methods return {@code void}.
065 *
066 * <pre class="code">public interface MessageContentsDelegate {
067 *    void handleMessage(String text);
068 *    void handleMessage(Map map);
069 *    void handleMessage(byte[] bytes);
070 *    void handleMessage(Serializable obj);
071 * }</pre>
072 *
073 * This next example handles all {@code Message} types and gets
074 * passed the actual (raw) {@code Message} as an argument. Again, no
075 * {@code Message} will be sent back as all of these methods return
076 * {@code void}.
077 *
078 * <pre class="code">public interface RawMessageDelegate {
079 *    void handleMessage(TextMessage message);
080 *    void handleMessage(MapMessage message);
081 *    void handleMessage(BytesMessage message);
082 *    void handleMessage(ObjectMessage message);
083 * }</pre>
084 *
085 * This next example illustrates a {@code Message} delegate
086 * that just consumes the {@code String} contents of
087 * {@link javax.jms.TextMessage TextMessages}. Notice also how the
088 * name of the {@code Message} handling method is different from the
089 * {@link #ORIGINAL_DEFAULT_LISTENER_METHOD original} (this will have to
090 * be configured in the attandant bean definition). Again, no {@code Message}
091 * will be sent back as the method returns {@code void}.
092 *
093 * <pre class="code">public interface TextMessageContentDelegate {
094 *    void onMessage(String text);
095 * }</pre>
096 *
097 * This final example illustrates a {@code Message} delegate
098 * that just consumes the {@code String} contents of
099 * {@link javax.jms.TextMessage TextMessages}. Notice how the return type
100 * of this method is {@code String}: This will result in the configured
101 * {@link MessageListenerAdapter} sending a {@link javax.jms.TextMessage} in response.
102 *
103 * <pre class="code">public interface ResponsiveTextMessageContentDelegate {
104 *    String handleMessage(String text);
105 * }</pre>
106 *
107 * For further examples and discussion please do refer to the Spring
108 * reference documentation which describes this class (and it's attendant
109 * XML configuration) in detail.
110 *
111 * @author Juergen Hoeller
112 * @since 2.0
113 * @see #setDelegate
114 * @see #setDefaultListenerMethod
115 * @see #setDefaultResponseDestination
116 * @see #setMessageConverter
117 * @see org.springframework.jms.support.converter.SimpleMessageConverter
118 * @see org.springframework.jms.listener.SessionAwareMessageListener
119 * @see org.springframework.jms.listener.AbstractMessageListenerContainer#setMessageListener
120 */
121public class MessageListenerAdapter extends AbstractAdaptableMessageListener implements SubscriptionNameProvider {
122
123        /**
124         * Out-of-the-box value for the default listener method: "handleMessage".
125         */
126        public static final String ORIGINAL_DEFAULT_LISTENER_METHOD = "handleMessage";
127
128
129        private Object delegate;
130
131        private String defaultListenerMethod = ORIGINAL_DEFAULT_LISTENER_METHOD;
132
133
134        /**
135         * Create a new {@link MessageListenerAdapter} with default settings.
136         */
137        @SuppressWarnings("deprecation")
138        public MessageListenerAdapter() {
139                initDefaultStrategies();
140                this.delegate = this;
141        }
142
143        /**
144         * Create a new {@link MessageListenerAdapter} for the given delegate.
145         * @param delegate the delegate object
146         */
147        @SuppressWarnings("deprecation")
148        public MessageListenerAdapter(Object delegate) {
149                initDefaultStrategies();
150                setDelegate(delegate);
151        }
152
153
154        /**
155         * Initialize the default implementations for the adapter's strategies.
156         * @deprecated as of 4.1, in favor of calling the corresponding setters
157         * in the subclass constructor
158         */
159        @Deprecated
160        protected void initDefaultStrategies() {
161        }
162
163        /**
164         * Set a target object to delegate message listening to.
165         * Specified listener methods have to be present on this target object.
166         * <p>If no explicit delegate object has been specified, listener
167         * methods are expected to present on this adapter instance, that is,
168         * on a custom subclass of this adapter, defining listener methods.
169         */
170        public void setDelegate(Object delegate) {
171                Assert.notNull(delegate, "Delegate must not be null");
172                this.delegate = delegate;
173        }
174
175        /**
176         * Return the target object to delegate message listening to.
177         */
178        protected Object getDelegate() {
179                return this.delegate;
180        }
181
182        /**
183         * Specify the name of the default listener method to delegate to,
184         * for the case where no specific listener method has been determined.
185         * Out-of-the-box value is {@link #ORIGINAL_DEFAULT_LISTENER_METHOD "handleMessage"}.
186         * @see #getListenerMethodName
187         */
188        public void setDefaultListenerMethod(String defaultListenerMethod) {
189                this.defaultListenerMethod = defaultListenerMethod;
190        }
191
192        /**
193         * Return the name of the default listener method to delegate to.
194         */
195        protected String getDefaultListenerMethod() {
196                return this.defaultListenerMethod;
197        }
198
199
200        /**
201         * Spring {@link SessionAwareMessageListener} entry point.
202         * <p>Delegates the message to the target listener method, with appropriate
203         * conversion of the message argument. If the target method returns a
204         * non-null object, wrap in a JMS message and send it back.
205         * @param message the incoming JMS message
206         * @param session the JMS session to operate on
207         * @throws JMSException if thrown by JMS API methods
208         */
209        @Override
210        @SuppressWarnings("unchecked")
211        public void onMessage(Message message, Session session) throws JMSException {
212                // Check whether the delegate is a MessageListener impl itself.
213                // In that case, the adapter will simply act as a pass-through.
214                Object delegate = getDelegate();
215                if (delegate != this) {
216                        if (delegate instanceof SessionAwareMessageListener) {
217                                if (session != null) {
218                                        ((SessionAwareMessageListener<Message>) delegate).onMessage(message, session);
219                                        return;
220                                }
221                                else if (!(delegate instanceof MessageListener)) {
222                                        throw new javax.jms.IllegalStateException("MessageListenerAdapter cannot handle a " +
223                                                        "SessionAwareMessageListener delegate if it hasn't been invoked with a Session itself");
224                                }
225                        }
226                        if (delegate instanceof MessageListener) {
227                                ((MessageListener) delegate).onMessage(message);
228                                return;
229                        }
230                }
231
232                // Regular case: find a handler method reflectively.
233                Object convertedMessage = extractMessage(message);
234                String methodName = getListenerMethodName(message, convertedMessage);
235                if (methodName == null) {
236                        throw new javax.jms.IllegalStateException("No default listener method specified: " +
237                                        "Either specify a non-null value for the 'defaultListenerMethod' property or " +
238                                        "override the 'getListenerMethodName' method.");
239                }
240
241                // Invoke the handler method with appropriate arguments.
242                Object[] listenerArguments = buildListenerArguments(convertedMessage);
243                Object result = invokeListenerMethod(methodName, listenerArguments);
244                if (result != null) {
245                        handleResult(result, message, session);
246                }
247                else {
248                        logger.trace("No result object given - no result to handle");
249                }
250        }
251
252        @Override
253        public String getSubscriptionName() {
254                Object delegate = getDelegate();
255                if (delegate != this && delegate instanceof SubscriptionNameProvider) {
256                        return ((SubscriptionNameProvider) delegate).getSubscriptionName();
257                }
258                else {
259                        return delegate.getClass().getName();
260                }
261        }
262
263        /**
264         * Determine the name of the listener method that is supposed to
265         * handle the given message.
266         * <p>The default implementation simply returns the configured
267         * default listener method, if any.
268         * @param originalMessage the JMS request message
269         * @param extractedMessage the converted JMS request message,
270         * to be passed into the listener method as argument
271         * @return the name of the listener method (never {@code null})
272         * @throws JMSException if thrown by JMS API methods
273         * @see #setDefaultListenerMethod
274         */
275        protected String getListenerMethodName(Message originalMessage, Object extractedMessage) throws JMSException {
276                return getDefaultListenerMethod();
277        }
278
279        /**
280         * Build an array of arguments to be passed into the target listener method.
281         * Allows for multiple method arguments to be built from a single message object.
282         * <p>The default implementation builds an array with the given message object
283         * as sole element. This means that the extracted message will always be passed
284         * into a <i>single</i> method argument, even if it is an array, with the target
285         * method having a corresponding single argument of the array's type declared.
286         * <p>This can be overridden to treat special message content such as arrays
287         * differently, for example passing in each element of the message array
288         * as distinct method argument.
289         * @param extractedMessage the content of the message
290         * @return the array of arguments to be passed into the
291         * listener method (each element of the array corresponding
292         * to a distinct method argument)
293         */
294        protected Object[] buildListenerArguments(Object extractedMessage) {
295                return new Object[] {extractedMessage};
296        }
297
298        /**
299         * Invoke the specified listener method.
300         * @param methodName the name of the listener method
301         * @param arguments the message arguments to be passed in
302         * @return the result returned from the listener method
303         * @throws JMSException if thrown by JMS API methods
304         * @see #getListenerMethodName
305         * @see #buildListenerArguments
306         */
307        protected Object invokeListenerMethod(String methodName, Object[] arguments) throws JMSException {
308                try {
309                        MethodInvoker methodInvoker = new MethodInvoker();
310                        methodInvoker.setTargetObject(getDelegate());
311                        methodInvoker.setTargetMethod(methodName);
312                        methodInvoker.setArguments(arguments);
313                        methodInvoker.prepare();
314                        return methodInvoker.invoke();
315                }
316                catch (InvocationTargetException ex) {
317                        Throwable targetEx = ex.getTargetException();
318                        if (targetEx instanceof JMSException) {
319                                throw (JMSException) targetEx;
320                        }
321                        else {
322                                throw new ListenerExecutionFailedException(
323                                                "Listener method '" + methodName + "' threw exception", targetEx);
324                        }
325                }
326                catch (Throwable ex) {
327                        throw new ListenerExecutionFailedException("Failed to invoke target method '" + methodName +
328                                        "' with arguments " + ObjectUtils.nullSafeToString(arguments), ex);
329                }
330        }
331
332}