001/*
002 * Copyright 2002-2015 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.remoting;
018
019import javax.jms.Connection;
020import javax.jms.ConnectionFactory;
021import javax.jms.JMSException;
022import javax.jms.Message;
023import javax.jms.MessageConsumer;
024import javax.jms.MessageFormatException;
025import javax.jms.MessageProducer;
026import javax.jms.Queue;
027import javax.jms.Session;
028import javax.jms.TemporaryQueue;
029
030import org.aopalliance.intercept.MethodInterceptor;
031import org.aopalliance.intercept.MethodInvocation;
032
033import org.springframework.aop.support.AopUtils;
034import org.springframework.beans.factory.InitializingBean;
035import org.springframework.jms.connection.ConnectionFactoryUtils;
036import org.springframework.jms.support.JmsUtils;
037import org.springframework.jms.support.converter.MessageConverter;
038import org.springframework.jms.support.converter.SimpleMessageConverter;
039import org.springframework.jms.support.destination.DestinationResolver;
040import org.springframework.jms.support.destination.DynamicDestinationResolver;
041import org.springframework.remoting.RemoteAccessException;
042import org.springframework.remoting.RemoteInvocationFailureException;
043import org.springframework.remoting.RemoteTimeoutException;
044import org.springframework.remoting.support.DefaultRemoteInvocationFactory;
045import org.springframework.remoting.support.RemoteInvocation;
046import org.springframework.remoting.support.RemoteInvocationFactory;
047import org.springframework.remoting.support.RemoteInvocationResult;
048
049/**
050 * {@link org.aopalliance.intercept.MethodInterceptor} for accessing a
051 * JMS-based remote service.
052 *
053 * <p>Serializes remote invocation objects and deserializes remote invocation
054 * result objects. Uses Java serialization just like RMI, but with the JMS
055 * provider as communication infrastructure.
056 *
057 * <p>To be configured with a {@link javax.jms.QueueConnectionFactory} and a
058 * target queue (either as {@link javax.jms.Queue} reference or as queue name).
059 *
060 * <p>Thanks to James Strachan for the original prototype that this
061 * JMS invoker mechanism was inspired by!
062 *
063 * @author Juergen Hoeller
064 * @author James Strachan
065 * @author Stephane Nicoll
066 * @since 2.0
067 * @see #setConnectionFactory
068 * @see #setQueue
069 * @see #setQueueName
070 * @see org.springframework.jms.remoting.JmsInvokerServiceExporter
071 * @see org.springframework.jms.remoting.JmsInvokerProxyFactoryBean
072 */
073public class JmsInvokerClientInterceptor implements MethodInterceptor, InitializingBean {
074
075        private ConnectionFactory connectionFactory;
076
077        private Object queue;
078
079        private DestinationResolver destinationResolver = new DynamicDestinationResolver();
080
081        private RemoteInvocationFactory remoteInvocationFactory = new DefaultRemoteInvocationFactory();
082
083        private MessageConverter messageConverter = new SimpleMessageConverter();
084
085        private long receiveTimeout = 0;
086
087
088        /**
089         * Set the QueueConnectionFactory to use for obtaining JMS QueueConnections.
090         */
091        public void setConnectionFactory(ConnectionFactory connectionFactory) {
092                this.connectionFactory = connectionFactory;
093        }
094
095        /**
096         * Return the QueueConnectionFactory to use for obtaining JMS QueueConnections.
097         */
098        protected ConnectionFactory getConnectionFactory() {
099                return this.connectionFactory;
100        }
101
102        /**
103         * Set the target Queue to send invoker requests to.
104         */
105        public void setQueue(Queue queue) {
106                this.queue = queue;
107        }
108
109        /**
110         * Set the name of target queue to send invoker requests to.
111         * <p>The specified name will be dynamically resolved via the
112         * {@link #setDestinationResolver DestinationResolver}.
113         */
114        public void setQueueName(String queueName) {
115                this.queue = queueName;
116        }
117
118        /**
119         * Set the DestinationResolver that is to be used to resolve Queue
120         * references for this accessor.
121         * <p>The default resolver is a {@code DynamicDestinationResolver}. Specify a
122         * {@code JndiDestinationResolver} for resolving destination names as JNDI locations.
123         * @see org.springframework.jms.support.destination.DynamicDestinationResolver
124         * @see org.springframework.jms.support.destination.JndiDestinationResolver
125         */
126        public void setDestinationResolver(DestinationResolver destinationResolver) {
127                this.destinationResolver =
128                                (destinationResolver != null ? destinationResolver : new DynamicDestinationResolver());
129        }
130
131        /**
132         * Set the {@link RemoteInvocationFactory} to use for this accessor.
133         * <p>Default is a {@link DefaultRemoteInvocationFactory}.
134         * <p>A custom invocation factory can add further context information
135         * to the invocation, for example user credentials.
136         */
137        public void setRemoteInvocationFactory(RemoteInvocationFactory remoteInvocationFactory) {
138                this.remoteInvocationFactory =
139                                (remoteInvocationFactory != null ? remoteInvocationFactory : new DefaultRemoteInvocationFactory());
140        }
141
142        /**
143         * Specify the {@link MessageConverter} to use for turning
144         * {@link org.springframework.remoting.support.RemoteInvocation}
145         * objects into request messages, as well as response messages into
146         * {@link org.springframework.remoting.support.RemoteInvocationResult} objects.
147         * <p>Default is a {@link SimpleMessageConverter}, using a standard JMS
148         * {@link javax.jms.ObjectMessage} for each invocation / invocation result
149         * object.
150         * <p>Custom implementations may generally adapt {@link java.io.Serializable}
151         * objects into special kinds of messages, or might be specifically tailored for
152         * translating {@code RemoteInvocation(Result)s} into specific kinds of messages.
153         */
154        public void setMessageConverter(MessageConverter messageConverter) {
155                this.messageConverter = (messageConverter != null ? messageConverter : new SimpleMessageConverter());
156        }
157
158        /**
159         * Set the timeout to use for receiving the response message for a request
160         * (in milliseconds).
161         * <p>The default is 0, which indicates a blocking receive without timeout.
162         * @see javax.jms.MessageConsumer#receive(long)
163         * @see javax.jms.MessageConsumer#receive()
164         */
165        public void setReceiveTimeout(long receiveTimeout) {
166                this.receiveTimeout = receiveTimeout;
167        }
168
169        /**
170         * Return the timeout to use for receiving the response message for a request
171         * (in milliseconds).
172         */
173        protected long getReceiveTimeout() {
174                return this.receiveTimeout;
175        }
176
177
178        @Override
179        public void afterPropertiesSet() {
180                if (getConnectionFactory() == null) {
181                        throw new IllegalArgumentException("Property 'connectionFactory' is required");
182                }
183                if (this.queue == null) {
184                        throw new IllegalArgumentException("'queue' or 'queueName' is required");
185                }
186        }
187
188
189        @Override
190        public Object invoke(MethodInvocation methodInvocation) throws Throwable {
191                if (AopUtils.isToStringMethod(methodInvocation.getMethod())) {
192                        return "JMS invoker proxy for queue [" + this.queue + "]";
193                }
194
195                RemoteInvocation invocation = createRemoteInvocation(methodInvocation);
196                RemoteInvocationResult result;
197                try {
198                        result = executeRequest(invocation);
199                }
200                catch (JMSException ex) {
201                        throw convertJmsInvokerAccessException(ex);
202                }
203                try {
204                        return recreateRemoteInvocationResult(result);
205                }
206                catch (Throwable ex) {
207                        if (result.hasInvocationTargetException()) {
208                                throw ex;
209                        }
210                        else {
211                                throw new RemoteInvocationFailureException("Invocation of method [" + methodInvocation.getMethod() +
212                                                "] failed in JMS invoker remote service at queue [" + this.queue + "]", ex);
213                        }
214                }
215        }
216
217        /**
218         * Create a new {@code RemoteInvocation} object for the given AOP method invocation.
219         * <p>The default implementation delegates to the {@link RemoteInvocationFactory}.
220         * <p>Can be overridden in subclasses to provide custom {@code RemoteInvocation}
221         * subclasses, containing additional invocation parameters like user credentials.
222         * Note that it is preferable to use a custom {@code RemoteInvocationFactory} which
223         * is a reusable strategy.
224         * @param methodInvocation the current AOP method invocation
225         * @return the RemoteInvocation object
226         * @see RemoteInvocationFactory#createRemoteInvocation
227         */
228        protected RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) {
229                return this.remoteInvocationFactory.createRemoteInvocation(methodInvocation);
230        }
231
232        /**
233         * Execute the given remote invocation, sending an invoker request message
234         * to this accessor's target queue and waiting for a corresponding response.
235         * @param invocation the RemoteInvocation to execute
236         * @return the RemoteInvocationResult object
237         * @throws JMSException in case of JMS failure
238         * @see #doExecuteRequest
239         */
240        protected RemoteInvocationResult executeRequest(RemoteInvocation invocation) throws JMSException {
241                Connection con = createConnection();
242                Session session = null;
243                try {
244                        session = createSession(con);
245                        Queue queueToUse = resolveQueue(session);
246                        Message requestMessage = createRequestMessage(session, invocation);
247                        con.start();
248                        Message responseMessage = doExecuteRequest(session, queueToUse, requestMessage);
249                        if (responseMessage != null) {
250                                return extractInvocationResult(responseMessage);
251                        }
252                        else {
253                                return onReceiveTimeout(invocation);
254                        }
255                }
256                finally {
257                        JmsUtils.closeSession(session);
258                        ConnectionFactoryUtils.releaseConnection(con, getConnectionFactory(), true);
259                }
260        }
261
262        /**
263         * Create a new JMS Connection for this JMS invoker.
264         */
265        protected Connection createConnection() throws JMSException {
266                return getConnectionFactory().createConnection();
267        }
268
269        /**
270         * Create a new JMS Session for this JMS invoker.
271         */
272        protected Session createSession(Connection con) throws JMSException {
273                return con.createSession(false, Session.AUTO_ACKNOWLEDGE);
274        }
275
276        /**
277         * Resolve this accessor's target queue.
278         * @param session the current JMS Session
279         * @return the resolved target Queue
280         * @throws JMSException if resolution failed
281         */
282        protected Queue resolveQueue(Session session) throws JMSException {
283                if (this.queue instanceof Queue) {
284                        return (Queue) this.queue;
285                }
286                else if (this.queue instanceof String) {
287                        return resolveQueueName(session, (String) this.queue);
288                }
289                else {
290                        throw new javax.jms.IllegalStateException(
291                                        "Queue object [" + this.queue + "] is neither a [javax.jms.Queue] nor a queue name String");
292                }
293        }
294
295        /**
296         * Resolve the given queue name into a JMS {@link javax.jms.Queue},
297         * via this accessor's {@link DestinationResolver}.
298         * @param session the current JMS Session
299         * @param queueName the name of the queue
300         * @return the located Queue
301         * @throws JMSException if resolution failed
302         * @see #setDestinationResolver
303         */
304        protected Queue resolveQueueName(Session session, String queueName) throws JMSException {
305                return (Queue) this.destinationResolver.resolveDestinationName(session, queueName, false);
306        }
307
308        /**
309         * Create the invoker request message.
310         * <p>The default implementation creates a JMS {@link javax.jms.ObjectMessage}
311         * for the given RemoteInvocation object.
312         * @param session the current JMS Session
313         * @param invocation the remote invocation to send
314         * @return the JMS Message to send
315         * @throws JMSException if the message could not be created
316         */
317        protected Message createRequestMessage(Session session, RemoteInvocation invocation) throws JMSException {
318                return this.messageConverter.toMessage(invocation, session);
319        }
320
321        /**
322         * Actually execute the given request, sending the invoker request message
323         * to the specified target queue and waiting for a corresponding response.
324         * <p>The default implementation is based on standard JMS send/receive,
325         * using a {@link javax.jms.TemporaryQueue} for receiving the response.
326         * @param session the JMS Session to use
327         * @param queue the resolved target Queue to send to
328         * @param requestMessage the JMS Message to send
329         * @return the RemoteInvocationResult object
330         * @throws JMSException in case of JMS failure
331         */
332        protected Message doExecuteRequest(Session session, Queue queue, Message requestMessage) throws JMSException {
333                TemporaryQueue responseQueue = null;
334                MessageProducer producer = null;
335                MessageConsumer consumer = null;
336                try {
337                        responseQueue = session.createTemporaryQueue();
338                        producer = session.createProducer(queue);
339                        consumer = session.createConsumer(responseQueue);
340                        requestMessage.setJMSReplyTo(responseQueue);
341                        producer.send(requestMessage);
342                        long timeout = getReceiveTimeout();
343                        return (timeout > 0 ? consumer.receive(timeout) : consumer.receive());
344                }
345                finally {
346                        JmsUtils.closeMessageConsumer(consumer);
347                        JmsUtils.closeMessageProducer(producer);
348                        if (responseQueue != null) {
349                                responseQueue.delete();
350                        }
351                }
352        }
353
354        /**
355         * Extract the invocation result from the response message.
356         * <p>The default implementation expects a JMS {@link javax.jms.ObjectMessage}
357         * carrying a {@link RemoteInvocationResult} object. If an invalid response
358         * message is encountered, the {@code onInvalidResponse} callback gets invoked.
359         * @param responseMessage the response message
360         * @return the invocation result
361         * @throws JMSException is thrown if a JMS exception occurs
362         * @see #onInvalidResponse
363         */
364        protected RemoteInvocationResult extractInvocationResult(Message responseMessage) throws JMSException {
365                Object content = this.messageConverter.fromMessage(responseMessage);
366                if (content instanceof RemoteInvocationResult) {
367                        return (RemoteInvocationResult) content;
368                }
369                return onInvalidResponse(responseMessage);
370        }
371
372        /**
373         * Callback that is invoked by {@link #executeRequest} when the receive
374         * timeout has expired for the specified {@link RemoteInvocation}.
375         * <p>By default, an {@link RemoteTimeoutException} is thrown. Sub-classes
376         * can choose to either throw a more dedicated exception or even return
377         * a default {@link RemoteInvocationResult} as a fallback.
378         * @param invocation the invocation
379         * @return a default result when the receive timeout has expired
380         */
381        protected RemoteInvocationResult onReceiveTimeout(RemoteInvocation invocation) {
382                throw new RemoteTimeoutException("Receive timeout after " + this.receiveTimeout + " ms for " + invocation);
383        }
384
385        /**
386         * Callback that is invoked by {@link #extractInvocationResult} when
387         * it encounters an invalid response message.
388         * <p>The default implementation throws a {@link MessageFormatException}.
389         * @param responseMessage the invalid response message
390         * @return an alternative invocation result that should be returned to
391         * the caller (if desired)
392         * @throws JMSException if the invalid response should lead to an
393         * infrastructure exception propagated to the caller
394         * @see #extractInvocationResult
395         */
396        protected RemoteInvocationResult onInvalidResponse(Message responseMessage) throws JMSException {
397                throw new MessageFormatException("Invalid response message: " + responseMessage);
398        }
399
400        /**
401         * Recreate the invocation result contained in the given {@link RemoteInvocationResult}
402         * object.
403         * <p>The default implementation calls the default {@code recreate()} method.
404         * <p>Can be overridden in subclasses to provide custom recreation, potentially
405         * processing the returned result object.
406         * @param result the RemoteInvocationResult to recreate
407         * @return a return value if the invocation result is a successful return
408         * @throws Throwable if the invocation result is an exception
409         * @see org.springframework.remoting.support.RemoteInvocationResult#recreate()
410         */
411        protected Object recreateRemoteInvocationResult(RemoteInvocationResult result) throws Throwable {
412                return result.recreate();
413        }
414
415        /**
416         * Convert the given JMS invoker access exception to an appropriate
417         * Spring {@link RemoteAccessException}.
418         * @param ex the exception to convert
419         * @return the RemoteAccessException to throw
420         */
421        protected RemoteAccessException convertJmsInvokerAccessException(JMSException ex) {
422                return new RemoteAccessException("Could not access JMS invoker queue [" + this.queue + "]", ex);
423        }
424
425}