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