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