From 90379aced95926ab1aec3a625ee73064a1255783 Mon Sep 17 00:00:00 2001 From: markpollack Date: Wed, 16 Jul 2008 19:12:51 +0000 Subject: [PATCH] Initial MSMQ support --- doc/reference/src/index.xml | 17 +- doc/reference/src/msmq.xml | 19 +- .../Adapter/MessageListenerAdapter.cs | 1 - src/Spring/Spring.Messaging/AssemblyInfo.cs | 4 + .../Core/DefaultMessageQueueFactory.cs | 94 +++ .../Messaging/Core/IMessageQueueFactory.cs | 41 ++ .../Messaging/Core/IMessageQueueOperations.cs | 160 +++++ ...ocallyExposedMessageQueueResourceHolder.cs | 37 + .../Core/MessagePostProcessorDelegate.cs | 36 + .../Core/MessageQueueResourceHolder.cs | 59 ++ .../Messaging/Core/MessageQueueTemplate.cs | 409 +++++++++++ .../Core/MessageQueueTransactionManager.cs | 192 ++++++ .../Listener/AbstractListenerContainer.cs | 244 +++++++ .../AbstractMessageListenerContainer.cs | 197 ++++++ ...AbstractPeekingMessageListenerContainer.cs | 440 ++++++++++++ .../AbstractSendToQueueExceptionHandler.cs | 99 +++ ...ctTransactionalMessageListenerContainer.cs | 120 ++++ .../DistributedTxMessageListenerContainer.cs | 198 ++++++ ...IDistributedTransactionExceptionHandler.cs | 66 ++ .../Messaging/Listener/IExceptionHandler.cs | 47 ++ .../Messaging/Listener/IMessageListener.cs | 37 + .../IMessageTransactionExceptionHandler.cs | 52 ++ .../Listener/ListenerClassDiagram.cd | 55 ++ .../Listener/MessageListenerAdapter.cs | 362 ++++++++++ ...onTransactionalMessageListenerContainer.cs | 158 +++++ ...eDistributedTransactionExceptionHandler.cs | 117 ++++ .../Listener/SendToQueueExceptionHandler.cs | 182 +++++ .../SimpleMessageListenerContainer.cs | 229 ++++++ .../Messaging/Listener/TransactionAction.cs | 38 + .../TransactionalMessageListenerContainer.cs | 649 ++++++++++++++++++ .../Messaging/MessagingException.cs | 59 ++ .../Converters/ActiveXMessageConverter.cs | 70 ++ .../Converters/BinaryMessageConverter.cs | 85 +++ .../Support/Converters/IMessageConverter.cs | 42 ++ .../Converters/XmlDocumentConverter.cs | 46 ++ .../Support/Converters/XmlMessageConverter.cs | 90 +++ .../Support/MessageQueueFactoryObject.cs | 188 +++++ .../Messaging/Support/QueueUtils.cs | 151 ++++ .../Spring.Messaging.2005.csproj | 109 +++ .../Spring.Messaging/Spring.Messaging.build | 32 + .../Core/MessageQueueTemplateTests.cs | 182 +++++ .../Core/MessageQueueTemplateTests.xml | 57 ++ .../Messaging/Core/ThreadingTests.cs | 56 ++ ...tributedTxMessageListenerContainerTests.cs | 116 ++++ ...ributedTxMessageListenerContainerTests.xml | 86 +++ .../Listener/LoggingExceptionHandler.cs | 68 ++ ...nsactionalMessageListenerContainerTests.cs | 114 +++ ...sactionalMessageListenerContainerTests.xml | 70 ++ .../Listener/SimpleExceptionHandler.cs | 36 + .../Messaging/Listener/SimpleHandler.cs | 37 + .../Listener/SimpleMessageListener.cs | 40 ++ .../SimpleMessageListenerContainerTests.cs | 106 +++ .../SimpleMessageListenerContainerTests.xml | 34 + ...nsactionalMessageListenerContainerTests.cs | 135 ++++ ...sactionalMessageListenerContainerTests.xml | 88 +++ .../MessageAlreadyProcessedException.cs | 65 ++ .../Support/MessageQueueFactoryObjectTests.cs | 89 +++ .../Messaging/queue-context.xml | 17 + .../Properties/AssemblyInfo.cs | 35 + .../Spring.Messaging.Tests.2005.csproj | 125 ++++ .../Spring.Messaging.Tests.build | 44 ++ .../Spring.Messaging.Tests.dll.config | 68 ++ 62 files changed, 6887 insertions(+), 12 deletions(-) create mode 100644 src/Spring/Spring.Messaging/AssemblyInfo.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Core/DefaultMessageQueueFactory.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Core/IMessageQueueFactory.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Core/IMessageQueueOperations.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Core/LocallyExposedMessageQueueResourceHolder.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Core/MessagePostProcessorDelegate.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Core/MessageQueueResourceHolder.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Core/MessageQueueTemplate.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Core/MessageQueueTransactionManager.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/AbstractListenerContainer.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/AbstractMessageListenerContainer.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/AbstractPeekingMessageListenerContainer.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/AbstractSendToQueueExceptionHandler.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/AbstractTransactionalMessageListenerContainer.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/DistributedTxMessageListenerContainer.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/IDistributedTransactionExceptionHandler.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/IExceptionHandler.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/IMessageListener.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/IMessageTransactionExceptionHandler.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/ListenerClassDiagram.cd create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/MessageListenerAdapter.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/NonTransactionalMessageListenerContainer.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/SendToQueueDistributedTransactionExceptionHandler.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/SendToQueueExceptionHandler.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/SimpleMessageListenerContainer.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/TransactionAction.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Listener/TransactionalMessageListenerContainer.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/MessagingException.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Support/Converters/ActiveXMessageConverter.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Support/Converters/BinaryMessageConverter.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Support/Converters/IMessageConverter.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Support/Converters/XmlDocumentConverter.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Support/Converters/XmlMessageConverter.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Support/MessageQueueFactoryObject.cs create mode 100644 src/Spring/Spring.Messaging/Messaging/Support/QueueUtils.cs create mode 100644 src/Spring/Spring.Messaging/Spring.Messaging.2005.csproj create mode 100644 src/Spring/Spring.Messaging/Spring.Messaging.build create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Core/MessageQueueTemplateTests.cs create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Core/MessageQueueTemplateTests.xml create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Core/ThreadingTests.cs create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Listener/DistributedTxMessageListenerContainerTests.cs create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Listener/DistributedTxMessageListenerContainerTests.xml create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Listener/LoggingExceptionHandler.cs create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Listener/NonTransactionalMessageListenerContainerTests.cs create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Listener/NonTransactionalMessageListenerContainerTests.xml create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleExceptionHandler.cs create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleHandler.cs create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleMessageListener.cs create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleMessageListenerContainerTests.cs create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleMessageListenerContainerTests.xml create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Listener/TransactionalMessageListenerContainerTests.cs create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Listener/TransactionalMessageListenerContainerTests.xml create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Support/MessageAlreadyProcessedException.cs create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/Support/MessageQueueFactoryObjectTests.cs create mode 100644 test/Spring/Spring.Messaging.Tests/Messaging/queue-context.xml create mode 100644 test/Spring/Spring.Messaging.Tests/Properties/AssemblyInfo.cs create mode 100644 test/Spring/Spring.Messaging.Tests/Spring.Messaging.Tests.2005.csproj create mode 100644 test/Spring/Spring.Messaging.Tests/Spring.Messaging.Tests.build create mode 100644 test/Spring/Spring.Messaging.Tests/Spring.Messaging.Tests.dll.config diff --git a/doc/reference/src/index.xml b/doc/reference/src/index.xml index 8b903cd6..6d8a82c3 100644 --- a/doc/reference/src/index.xml +++ b/doc/reference/src/index.xml @@ -17,6 +17,7 @@ + @@ -53,8 +54,8 @@ The Spring.NET Framework Reference Documentation - Version 1.1.2 - Last Updated June 12, 2008 + Version 1.2.0 M1 + Last Updated July 16, 2008 Mark @@ -288,7 +289,7 @@ &services; &webservices; - &nms; + &msmq; + - --> + VS.NET Integration diff --git a/doc/reference/src/msmq.xml b/doc/reference/src/msmq.xml index ec57464e..0f5a1aa0 100644 --- a/doc/reference/src/msmq.xml +++ b/doc/reference/src/msmq.xml @@ -71,7 +71,7 @@ poison-message handling policies. The message listener container leverages Spring's support for managing transactions. Both DTC, local messaging transactions, and local database transactions are supported. In - particular, you can easily coordinating the commit and rollback of a local + particular, you can easily coordinate the commit and rollback of a local MessageQueueTransaction and a local database transaction when they are used together. @@ -170,7 +170,10 @@ MessageQueueTransactionManager an implementation of Spring's IPlatformTransactionManager abstraction that provides a uniform API on top of various transaction manager - (ADO.NET,NHibernate, MSMQ, etc). + (ADO.NET,NHibernate, MSMQ, etc). Spring's + MessageQueueTransactionManager is responsible for + createing, committing, and rolling back a MSMQ + MessageQueueTransaction. While you can create the message listener container programmatically, we will show the declarative configuration approach @@ -283,11 +286,13 @@ queue questionTxQueue for redelivery). If the same message causes an exception in processing 5 times ,then it will be sent transactionally to the retryQuestionTxQueue and the message transaction will commit (removing - it from the queue questionTxQueue). The SendToQueueExceptionHandler - implements the interface - IMessageTransactionExceptionHandler (discussed - below) so you can write your own implementations should the provided ones - not meet your needs. + it from the queue questionTxQueue). You can also specify that certain + exceptions should commit the transaction (remove from the queue) but this + is not shown here ,see below for more informatio non this functionality + The SendToQueueExceptionHandler implements the + interface IMessageTransactionExceptionHandler + (discussed below) so you can write your own implementations should the + provided ones not meet your needs. That's the quick tour folks. Hopefully you got a general feel for how things work, what requires configuration, and what is the code you diff --git a/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Listener/Adapter/MessageListenerAdapter.cs b/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Listener/Adapter/MessageListenerAdapter.cs index eb361a07..a081f689 100644 --- a/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Listener/Adapter/MessageListenerAdapter.cs +++ b/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Listener/Adapter/MessageListenerAdapter.cs @@ -179,7 +179,6 @@ namespace Spring.Messaging.Nms.Listener.Adapter } else { - //TODO ' use as ' IMessage msg = result as IMessage; if (msg == null) { diff --git a/src/Spring/Spring.Messaging/AssemblyInfo.cs b/src/Spring/Spring.Messaging/AssemblyInfo.cs new file mode 100644 index 00000000..be98d163 --- /dev/null +++ b/src/Spring/Spring.Messaging/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Reflection; + +[assembly: AssemblyTitle("Spring.Net MSMQ Messaging support")] +[assembly: AssemblyDescription("Interfaces and classes that provide MSMQ 3.0 support in Spring.Net")] \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Core/DefaultMessageQueueFactory.cs b/src/Spring/Spring.Messaging/Messaging/Core/DefaultMessageQueueFactory.cs new file mode 100644 index 00000000..46f86eb2 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Core/DefaultMessageQueueFactory.cs @@ -0,0 +1,94 @@ +#region License + +/* + * Copyright © 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + + +using System.Collections; +using System.Messaging; +using Spring.Context; +using Spring.Messaging.Support.Converters; +using Spring.Threading; +using Spring.Util; + +namespace Spring.Messaging.Core +{ + /// + /// A implementation that caches MessageQueue and IMessageConverter + /// instances. + /// + /// Mark Pollack + public class DefaultMessageQueueFactory : IMessageQueueFactory, IApplicationContextAware + { + private static readonly string QUEUE_DICTIONARY_SLOTNAME = + UniqueKey.GetTypeScopedString(typeof (DefaultMessageQueueFactory), "Queue"); + + private static readonly string CONVERTER_DICTIONARY_SLOTNAME = + UniqueKey.GetTypeScopedString(typeof (DefaultMessageQueueFactory), "Converter"); + + private IApplicationContext applicationContext; + + #region IMessageQueueFactory Members + + public MessageQueue CreateMessageQueue(string messageQueueObjectName) + { + AssertUtils.ArgumentHasText(messageQueueObjectName, "DefaultMessageQueueObjectName"); + IDictionary queues = LogicalThreadContext.GetData(QUEUE_DICTIONARY_SLOTNAME) as IDictionary; + if (queues == null) + { + queues = new Hashtable(); + LogicalThreadContext.SetData(QUEUE_DICTIONARY_SLOTNAME, queues); + } + if (!queues.Contains(messageQueueObjectName)) + { + MessageQueue mq = ApplicationContext.GetObject(messageQueueObjectName) as MessageQueue; + queues.Add(messageQueueObjectName, mq); + } + return queues[messageQueueObjectName] as MessageQueue; + } + + public IMessageConverter CreateMessageConverter(string messgaeConverterObjectName) + { + AssertUtils.ArgumentHasText(messgaeConverterObjectName, "MessgaeFormatterObjectName"); + IDictionary converters = LogicalThreadContext.GetData(CONVERTER_DICTIONARY_SLOTNAME) as IDictionary; + if (converters == null) + { + converters = new Hashtable(); + LogicalThreadContext.SetData(CONVERTER_DICTIONARY_SLOTNAME, converters); + } + if (!converters.Contains(messgaeConverterObjectName)) + { + IMessageConverter mc = ApplicationContext.GetObject(messgaeConverterObjectName) as IMessageConverter; + converters.Add(messgaeConverterObjectName, mc); + } + return converters[messgaeConverterObjectName] as IMessageConverter; + } + + #endregion + + #region IApplicationContextAware Members + + public IApplicationContext ApplicationContext + { + get { return applicationContext; } + set { applicationContext = value; } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Core/IMessageQueueFactory.cs b/src/Spring/Spring.Messaging/Messaging/Core/IMessageQueueFactory.cs new file mode 100644 index 00000000..1326beba --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Core/IMessageQueueFactory.cs @@ -0,0 +1,41 @@ +#region License + +/* + * Copyright © 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System.Messaging; +using Spring.Messaging.Support.Converters; + +namespace Spring.Messaging.Core +{ + /// + /// An interface for creating MessageQueue and IMessageConverter objects. + /// + /// + /// These objects have methods that are generally not thread safe, (IMessageConverter classes + /// rely on IMessageFormatter objects that are not thread safe). A major reason to + /// for this interface is to provide thread-local instances such that appliation code need + /// not be concerned with these resource management issues. + /// + public interface IMessageQueueFactory + { + MessageQueue CreateMessageQueue(string messageQueueObjectName); + + IMessageConverter CreateMessageConverter(string messageConverterObjectName); + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Core/IMessageQueueOperations.cs b/src/Spring/Spring.Messaging/Messaging/Core/IMessageQueueOperations.cs new file mode 100644 index 00000000..b28af25b --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Core/IMessageQueueOperations.cs @@ -0,0 +1,160 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System.Messaging; +using Spring.Messaging.Support.Converters; + +namespace Spring.Messaging.Core +{ + /// + /// Specifies a basic set of helper MSMQ opertions. + /// + /// + /// Implemented by . Not often used but a useful option + /// to enhance testability, as it can easily be mocked or stubbed. + /// + /// + /// Provides MessageQueueTemplate's + /// Send(..) and receive(..) methods that mirror various MSMQ MessageQueue + /// API methods. + /// + /// + /// Mark Pollack + public interface IMessageQueueOperations + { + /// + /// Send the given object to the default destination, converting the object + /// to a MSMQ message with a configured IMessageConverter. + /// + /// This will only work with a default destination queue specified! + /// The obj. + void ConvertAndSend(object obj); + + /// Send the given object to the default destination, converting the object + /// to a MSMQ message with a configured IMessageConverter. The IMessagePostProcessor + /// callback allows for modification of the message after conversion. + ///

This will only work with a default destination specified!

+ ///
+ /// the object to convert to a message + /// + /// the callback to modify the message + /// + /// if thrown by MSMQ API methods + void ConvertAndSend(object obj, MessagePostProcessorDelegate messagePostProcessorDelegate); + + /// Send the given object to the specified destination, converting the object + /// to a MSMQ message with a configured and resolving the + /// destination name to a using a + /// + /// the name of the destination queue + /// to send this message to (to be resolved to an actual MessageQueue + /// by a IMessageQueueFactory) + /// + /// the object to convert to a message + /// + /// NMSException if there is any problem + void ConvertAndSend(string messageQueueObjectName, object obj); + + /// Send the given object to the specified destination, converting the object + /// to a MSMQ message with a configured and resolving the + /// destination name to a with an + /// The callback allows for modification of the message after conversion. + /// + /// the name of the destination queue + /// to send this message to (to be resolved to an actual MessageQueue + /// by a IMessageQueueFactory) + /// + /// the object to convert to a message + /// + /// the callback to modify the message + /// + /// if thrown by MSMQ API methods + void ConvertAndSend(string messageQueueObjectName, object obj, MessagePostProcessorDelegate messagePostProcessorDelegate); + + /// + /// Receive and convert a message synchronously from the default message queue. + /// + /// The converted object + /// if thrown by MSMQ API methods. Note an + /// exception will be thrown if the timeout of the syncrhonous recieve operation expires. + /// + object ReceiveAndConvert(); + + + /// + /// Receives and convert a message synchronously from the specified message queue. + /// + /// Name of the message queue object. + /// the converted object + /// if thrown by MSMQ API methods. Note an + /// exception will be thrown if the timeout of the syncrhonous recieve operation expires. + /// + object ReceiveAndConvert(string messageQueueObjectName); + + /// + /// Receives a message on the default message queue using the transactional settings as dicted by MessageQueue's Transactional property and + /// the current Spring managed ambient transaction. + /// + /// A message. + Message Receive(); + + /// + /// Receives a message on the specified queue using the transactional settings as dicted by MessageQueue's Transactional property and + /// the current Spring managed ambient transaction. + /// + /// Name of the message queue object. + /// + Message Receive(string messageQueueObjectName); + + /// + /// Sends the specified message to the default message queue using the + /// transactional settings as dicted by MessageQueue's Transactional property and + /// the current Spring managed ambient transaction. + /// + /// The message to send + void Send(Message message); + + + /// + /// Sends the specified message to the message queue using the + /// transactional settings as dicted by MessageQueue's Transactional property and + /// the current Spring managed ambient transaction. + /// + /// Name of the message queue object. + /// The message. + void Send(string messageQueueObjectName, Message message); + + /// + /// Sends the specified message on the provided MessageQueue using the + /// transactional settings as dicted by MessageQueue's Transactional property and + /// the current Spring managed ambient transaction. + /// + /// + /// Note that it is the callers responsibility to ensure that the MessageQueue instance + /// passed into this not being access simultaneously by other threads. + /// + /// A transactional send (either local or DTC transaction) will be + /// attempted for a transacitonal queue, falling back to a single-transaction send + /// to a transactional queue if there is not ambient Spring managed transaction. + /// The DefaultMessageQueue to send a message to. + /// The message to send + void Send(MessageQueue messageQueue, Message message); + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Core/LocallyExposedMessageQueueResourceHolder.cs b/src/Spring/Spring.Messaging/Messaging/Core/LocallyExposedMessageQueueResourceHolder.cs new file mode 100644 index 00000000..0a98539b --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Core/LocallyExposedMessageQueueResourceHolder.cs @@ -0,0 +1,37 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System.Messaging; + +namespace Spring.Messaging.Core +{ + /// + /// MessageQueueResourceHolder marker subclass that indicates local exposure, + /// i.e. that does not indicate an externally managed transaction. + /// + /// Mark Pollack + public class LocallyExposedMessageQueueResourceHolder : MessageQueueResourceHolder + { + public LocallyExposedMessageQueueResourceHolder(MessageQueueTransaction messageQueueTransaction) + : base(messageQueueTransaction) + { + } + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Core/MessagePostProcessorDelegate.cs b/src/Spring/Spring.Messaging/Messaging/Core/MessagePostProcessorDelegate.cs new file mode 100644 index 00000000..39fb3129 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Core/MessagePostProcessorDelegate.cs @@ -0,0 +1,36 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System.Messaging; + +namespace Spring.Messaging.Core +{ + /// + /// To be used with MessageQueueTemplate's send method that + /// convert an object to a message. + /// + /// + /// It allows for further modification of the message after it has been processed + /// by the converter. This is useful for setting of Message properties (e.g. + /// CorrelationId, AppSpecific, TimeToReachQueue). + /// + /// Mark Pollack + public delegate Message MessagePostProcessorDelegate(Message message); +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Core/MessageQueueResourceHolder.cs b/src/Spring/Spring.Messaging/Messaging/Core/MessageQueueResourceHolder.cs new file mode 100644 index 00000000..5ab88676 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Core/MessageQueueResourceHolder.cs @@ -0,0 +1,59 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System.Messaging; +using Spring.Transaction.Support; + +namespace Spring.Messaging.Core +{ + /// + /// MessageQueue resource holder, wrapping a MessageQueueTransaction. + /// MessageQueueTransactionManager binds instances of this class to the thread. + /// + /// + /// This is an SPI class, not intended to be used by applications. + /// + /// Mark Pollack + public class MessageQueueResourceHolder : ResourceHolderSupport + { + private MessageQueueTransaction messageQueueTransaction; + + + /// + /// Initializes a new instance of the class. + /// + /// The message queue transaction. + public MessageQueueResourceHolder(MessageQueueTransaction messageQueueTransaction) + { + this.messageQueueTransaction = messageQueueTransaction; + } + + + /// + /// Gets or sets the message queue transaction. + /// + /// The message queue transaction. + public MessageQueueTransaction MessageQueueTransaction + { + get { return messageQueueTransaction; } + set { messageQueueTransaction = value; } + } + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Core/MessageQueueTemplate.cs b/src/Spring/Spring.Messaging/Messaging/Core/MessageQueueTemplate.cs new file mode 100644 index 00000000..9fa8b5f5 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Core/MessageQueueTemplate.cs @@ -0,0 +1,409 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Messaging; +using Common.Logging; +using Spring.Context; +using Spring.Messaging.Support; +using Spring.Messaging.Support.Converters; +using Spring.Objects.Factory; + +namespace Spring.Messaging.Core +{ + /// + /// Helper class that simplifies MSMQ access code. + /// + /// + /// + /// Using the System.Messaging.MessageQueue class directly in application code has a number of + /// shortcomings, namely that most operations are not thread safe (in particular Send) and + /// IMessageFormatter classes are not thread safe either. + /// + /// + /// The MessageQueueTemplate class overcomes these limitations letting you use a single instance + /// of MessageQueueTemplate across multiple threads to perform standard MessageQueue opertations. + /// Classes that are not thread safe are obtained and cached in thread local storage via an + /// implementation of the interface, specifically + /// . + /// + /// + /// You can access the thread local instance of the MessageQueue associated with this template + /// via the Property DefaultMessageQueue. + /// + /// + /// The template's Send methods will select an appropriate transaction delivery settings so + /// calling code does not need to explicitly manage this responsibility themselves and thus + /// allowing for greater portability of code across different, but common, transactional usage scenarios. + /// + /// A transactional send (either local or DTC transaction) will be + /// attempted for a transacitonal queue, falling back to a single-transaction send + /// to a transactional queue if there is not ambient Spring managed transaction. + /// + /// The overloaded ConvertAndSend and ReceiveAndConvert methods inherit the transactional + /// semantics of the previously described Send method but more importantly, they help to ensure + /// that thread safe access to instances are + /// used as well as providing additional central location to put programmic logic that translates + /// between the MSMQ Message object and the your business objects. This for example is useful if you + /// need to perform additional translation operations after calling a IMessageFormatter instance or + /// want to directly extract and process the Message body contents. + /// + /// + public class MessageQueueTemplate : IMessageQueueOperations, IInitializingObject, IApplicationContextAware + { + #region Logging Definition + + private static readonly ILog LOG = LogManager.GetLogger(typeof (MessageQueueTemplate)); + + #endregion + + #region Fields + + private string defaultMessageQueueObjectName; + private string messageConverterObjectName; + + private IMessageQueueFactory messageQueueFactory; + private IApplicationContext applicationContext; + + private TimeSpan timeout = MessageQueue.InfiniteTimeout; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public MessageQueueTemplate() + { + } + + + /// + /// Initializes a new instance of the class. + /// + /// Name of the message queue as registered in the Spring container. + public MessageQueueTemplate(string messageQueueName) + { + defaultMessageQueueObjectName = messageQueueName; + } + + #endregion + + #region Properties + + /// + /// Gets or sets the message queue factory to use for creating MessageQueue and IMessageConverters. + /// Default value is one that support thread local instances. + /// + /// The message queue factory. + public IMessageQueueFactory MessageQueueFactory + { + get { return messageQueueFactory; } + set { messageQueueFactory = value; } + } + + /// + /// Gets or sets the name of the default message queue as identified in the Spring container. + /// + /// The name of the message queue as identified in the Spring container. + public string DefaultMessageQueueObjectName + { + get { return defaultMessageQueueObjectName; } + set { defaultMessageQueueObjectName = value; } + } + + /// + /// Gets or sets the name of the message converter object. The name will be passed to + /// the class to resolve it to an actual MessageQueue + /// instance. + /// + /// The default name is internally generated and will register an XmlMessageConverter + /// that uses an and a simple System.String as its TargetType. + /// The name of the message converter object. + public string MessageConverterObjectName + { + get { return messageConverterObjectName; } + set { messageConverterObjectName = value; } + } + + /// + /// Gets the default message queue to be used on send/receive operations that do not + /// have a destination parameter. The MessageQueue instance is resolved using + /// the template's , the default implementaion + /// will return an unique instance per thread. + /// + /// The default message queue. + public MessageQueue DefaultMessageQueue + { + get + { + return MessageQueueFactory.CreateMessageQueue(DefaultMessageQueueObjectName); + } + } + + + /// + /// Gets the message converter to use for this template. Used to resolve + /// object parameters to ConvertAndSend methods and object results + /// from ReceiveAndConvert methods. + /// + /// + /// The default + /// + /// The message converter. + public IMessageConverter MessageConverter + { + get + { + if (messageConverterObjectName == null) + { + throw new InvalidOperationException( + "No MessageConverter registered. Check configuration of MessageQueueTemplate."); + } + return messageQueueFactory.CreateMessageConverter(MessageConverterObjectName); + } + } + + + /// + /// Gets or sets the receive timeout to be used on recieve operations. Default value is + /// MessageQueue.InfiniteTimeout (which is actually ~3 months). + /// + /// The receive timeout. + public TimeSpan ReceiveTimeout + { + get { return timeout; } + set { timeout = value; } + } + + #region IApplicationContextAware Members + + + /// + /// Set the that this + /// object runs in. + /// + public IApplicationContext ApplicationContext + { + get { return applicationContext; } + set { applicationContext = value; } + } + + #endregion + + #endregion + + #region IInitializingObject Members + + + /// + /// Invoked by an + /// after it has injected all of an object's dependencies. + /// + /// + /// Ensure that the DefaultMessageQueueObjectName property is set, creates + /// a default implementation of the interface + /// () that retrieves instances on a per-thread + /// basis, and registers in the Spring container a default implementation of + /// () with a + /// simple System.String as its TargetType. + /// + public void AfterPropertiesSet() + { + if (DefaultMessageQueueObjectName == null) + { + throw new ArgumentException("DefaultMessageQueueObjectName is required."); + } + if (MessageQueueFactory == null) + { + DefaultMessageQueueFactory mqf = new DefaultMessageQueueFactory(); + mqf.ApplicationContext = applicationContext; + messageQueueFactory = mqf; + } + if (messageConverterObjectName == null) + { + messageConverterObjectName = QueueUtils.RegisterDefaultMessageConverter(applicationContext); + } + } + + #endregion + + #region IMessageQueueOperations Members + + public void ConvertAndSend(object obj) + { + CheckDefaultMessageQueue(); + ConvertAndSend(DefaultMessageQueueObjectName, obj); + } + + public void ConvertAndSend(object obj, MessagePostProcessorDelegate messagePostProcessorDelegate) + { + CheckDefaultMessageQueue(); + ConvertAndSend(DefaultMessageQueueObjectName, obj, messagePostProcessorDelegate); + } + + public void ConvertAndSend(string messageQueueObjectName, object obj) + { + Message msg = MessageConverter.ToMessage(obj); + Send(MessageQueueFactory.CreateMessageQueue(messageQueueObjectName), msg); + } + + public void ConvertAndSend(string messageQueueObjectName, object obj, + MessagePostProcessorDelegate messagePostProcessorDelegate) + { + Message msg = MessageConverter.ToMessage(obj); + Message msgToSend = messagePostProcessorDelegate(msg); + Send(MessageQueueFactory.CreateMessageQueue(messageQueueObjectName), msgToSend); + } + + public object ReceiveAndConvert() + { + MessageQueue mq = DefaultMessageQueue; + Message m = mq.Receive(ReceiveTimeout); + return DoConvertMessage(m); + } + + public object ReceiveAndConvert(string messageQueueObjectName) + { + MessageQueue mq = MessageQueueFactory.CreateMessageQueue(messageQueueObjectName); + Message m = mq.Receive(ReceiveTimeout); + return DoConvertMessage(m); + } + + public Message Receive() + { + return DefaultMessageQueue.Receive(ReceiveTimeout); + } + + public Message Receive(string messageQueueObjectName) + { + return MessageQueueFactory.CreateMessageQueue(messageQueueObjectName).Receive(ReceiveTimeout); + } + + public void Send(Message message) + { + Send(DefaultMessageQueue, message); + } + + public void Send(string messageQueueObjectName, Message message) + { + Send(MessageQueueFactory.CreateMessageQueue(messageQueueObjectName), message); + } + + public virtual void Send(MessageQueue messageQueue, Message message) + { + DoSend(messageQueue, message); + } + + #endregion + + #region Protected Methods + + protected virtual void DoSend(MessageQueue messageQueue, Message message) + { + if (System.Transactions.Transaction.Current == null) + { + DoSendMessageQueueTransactional(messageQueue, message); + } + else + { + DoSendTxScope(messageQueue, message); + } + } + + protected virtual void DoSendMessageQueueTransactional(MessageQueue mq, Message msg) + { + MessageQueueTransaction transactionToUse = QueueUtils.GetMessageQueueTransaction(null); + if (mq.Transactional) + { + // DefaultMessageQueue transaction is externally managed. + if (transactionToUse != null) + { + if (LOG.IsDebugEnabled) + { + LOG.Debug( + "Sending messsage using externally managed MessageQueueTransction to transactional queue [" + + mq.QueueName + "]."); + } + mq.Send(msg, transactionToUse); + } + else + { + /* From MSDN documentation + * If a non-transactional message is sent to a transactional queue, + * this component creates a single-message transaction for it, + * except in the case of referencing a queue on a remote computer + * using a direct format name. In this situation, if you do not specify a + * transaction context when sending a message, one is not created for you + * and the message will be sent to the dead-letter queue.*/ + LOG.Warn("Sending message using implicit single-message transaction to transactional queue [" + + mq.QueueName + "]."); + mq.Send(msg, MessageQueueTransactionType.Single); + } + } + else + { + if (transactionToUse != null) + { + LOG.Warn("Thread local message transaction ignored for sending to non-transactional queue."); + mq.Send(msg); + } + else + { + if (LOG.IsDebugEnabled) + { + LOG.Debug("Sending messsage without MSMQ transaction to non-TX-QUEUE."); + } + //Typical case, non TLS transaction, non-tx queue. + mq.Send(msg); + } + } + } + + + protected virtual void DoSendTxScope(MessageQueue mq, Message msg) + { + mq.Send(msg, MessageQueueTransactionType.Automatic); + } + + protected virtual object DoConvertMessage(Message m) + { + if (m != null) + { + return MessageConverter.FromMessage(m); + } + else + { + return null; + } + } + + protected virtual void CheckDefaultMessageQueue() + { + if (DefaultMessageQueueObjectName == null) + { + throw new SystemException("No DefaultMessageQueueObjectName specified. Check configuration of MessageQueueTemplate."); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Core/MessageQueueTransactionManager.cs b/src/Spring/Spring.Messaging/Messaging/Core/MessageQueueTransactionManager.cs new file mode 100644 index 00000000..9ecc8dc3 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Core/MessageQueueTransactionManager.cs @@ -0,0 +1,192 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + + +using System.Messaging; +using Common.Logging; +using Spring.Data.Core; +using Spring.Transaction; +using Spring.Transaction.Support; +using Spring.Util; + +namespace Spring.Messaging.Core +{ + /// + /// implementation for MSMQ. Binds a + /// MessageQueueTransaction to the thread. + /// + /// + /// + /// This local strategy is an alternative to executing MSMQ operations within + /// DTC transactions. Its advantage is that multiple MSMQ operations can + /// easily participate within the same local MessagingTransaction transparently when + /// using the class for send and recieve operations + /// and not pay the overhead of a DTC transaction. + /// + /// Transaction synchronization is turned off by default, as this manager might + /// be used alongside a IDbProvider-based Spring transaction manager such as the + /// ADO.NET . + /// which has stronger needs for synchronization. + /// + /// Mark Pollack + public class MessageQueueTransactionManager : AbstractPlatformTransactionManager + { + public static readonly string CURRENT_TRANSACTION_SLOTNAME = + UniqueKey.GetTypeScopedString(typeof (MessageQueueTransaction), "Current"); + + #region Logging Definition + + private static readonly ILog LOG = LogManager.GetLogger(typeof (MessageQueueTransactionManager)); + + #endregion + + /// + /// Initializes a new instance of the class. + /// + /// + /// Turns off transaction synchronization by default, as this manager might + /// be used alongside a DbProvider-based Spring transaction manager like + /// AdoPlatformTransactionManager, which has stronger needs for synchronization. + /// Only one manager is allowed to drive synchronization at any point of time. + /// + public MessageQueueTransactionManager() + { + TransactionSynchronization = TransactionSynchronizationState.Never; + } + + protected override object DoGetTransaction() + { + MessageQueueTransactionObject txObject = new MessageQueueTransactionObject(); + txObject.ResourceHolder = + (MessageQueueResourceHolder) TransactionSynchronizationManager.GetResource(CURRENT_TRANSACTION_SLOTNAME); + return txObject; + } + + protected override bool IsExistingTransaction(object transaction) + { + MessageQueueTransactionObject txObject = (MessageQueueTransactionObject) transaction; + return (txObject.ResourceHolder != null); + } + + protected override void DoBegin(object transaction, ITransactionDefinition definition) + { + //TODO check isolation level is different than default value? + + MessageQueueTransactionObject txObject = (MessageQueueTransactionObject) transaction; + + MessageQueueTransaction mqt = new MessageQueueTransaction(); + mqt.Begin(); + + txObject.ResourceHolder = new MessageQueueResourceHolder(mqt); + txObject.ResourceHolder.SynchronizedWithTransaction = true; + + int timeout = DetermineTimeout(definition); + if (timeout != DefaultTransactionDefinition.TIMEOUT_DEFAULT) + { + txObject.ResourceHolder.TimeoutInSeconds = timeout; + } + TransactionSynchronizationManager.BindResource(CURRENT_TRANSACTION_SLOTNAME, txObject.ResourceHolder); + } + + protected override object DoSuspend(object transaction) + { + MessageQueueTransactionObject txObject = (MessageQueueTransactionObject) transaction; + txObject.ResourceHolder = null; + return TransactionSynchronizationManager.UnbindResource(CURRENT_TRANSACTION_SLOTNAME); + } + + protected override void DoResume(object transaction, object suspendedResources) + { + MessageQueueResourceHolder queueHolder = (MessageQueueResourceHolder) suspendedResources; + TransactionSynchronizationManager.BindResource(CURRENT_TRANSACTION_SLOTNAME, queueHolder); + } + + protected override void DoCommit(DefaultTransactionStatus status) + { + MessageQueueTransactionObject txObject = (MessageQueueTransactionObject) status.Transaction; + MessageQueueTransaction transaction = txObject.ResourceHolder.MessageQueueTransaction; + try + { + if (LOG.IsDebugEnabled) + { + LOG.Debug("Committing MessageQueueTransaction"); + } + transaction.Commit(); + } + catch (MessageQueueException ex) + { + throw new TransactionSystemException("Could not commit DefaultMessageQueue transaction", ex); + } + } + + protected override void DoRollback(DefaultTransactionStatus status) + { + MessageQueueTransactionObject txObject = (MessageQueueTransactionObject) status.Transaction; + MessageQueueTransaction transaction = txObject.ResourceHolder.MessageQueueTransaction; + try + { + if (LOG.IsDebugEnabled) + { + LOG.Debug("Committing MessageQueueTransaction"); + } + transaction.Abort(); + } + catch (MessageQueueException ex) + { + throw new TransactionSystemException("Could not roll back DefaultMessageQueue transaction", ex); + } + } + + protected override void DoSetRollbackOnly(DefaultTransactionStatus status) + { + MessageQueueTransactionObject txObject = (MessageQueueTransactionObject) status.Transaction; + txObject.ResourceHolder.RollbackOnly = true; + } + + protected override void DoCleanupAfterCompletion(object transaction) + { + MessageQueueTransactionObject txObject = (MessageQueueTransactionObject) transaction; + TransactionSynchronizationManager.UnbindResource(CURRENT_TRANSACTION_SLOTNAME); + txObject.ResourceHolder.Clear(); + } + + + private class MessageQueueTransactionObject : ISmartTransactionObject + { + private MessageQueueResourceHolder resourceHolder; + + + public MessageQueueResourceHolder ResourceHolder + { + get { return resourceHolder; } + set { resourceHolder = value; } + } + + #region ISmartTransactionObject Members + + public bool RollbackOnly + { + get { return resourceHolder.RollbackOnly; } + } + + #endregion + } + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/AbstractListenerContainer.cs b/src/Spring/Spring.Messaging/Messaging/Listener/AbstractListenerContainer.cs new file mode 100644 index 00000000..41d9221b --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/AbstractListenerContainer.cs @@ -0,0 +1,244 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + + +using System; +using System.Threading; +using Common.Logging; +using Spring.Objects.Factory; + +namespace Spring.Messaging.Listener +{ + /// + /// Provides basic lifecyle management methods for implementing a message listener container. + /// + /// + /// This base class does not assume any specific listener programming model + /// or listener invoker mechanism. It just provides the general runtime + /// lifecycle management needed for any kind of message-based listening mechanism. + /// + /// For a concrete listener programming model, check out the + /// subclass. For a concrete listener + /// invoker mechanism, check out the , + /// , or + /// classes. + /// + /// + /// Mark Pollack + public abstract class AbstractListenerContainer : IInitializingObject, IObjectNameAware, IDisposable + { + #region Logging Definition + + private static readonly ILog LOG = LogManager.GetLogger(typeof (AbstractListenerContainer)); + + #endregion + + private bool autoStartup = true; + + private string objectName; + private bool active; + private bool running; + private object lifecycleMonitor = new object(); + + #region Properties + + /// + /// Sets a value indicating whether to automatically start the container after initialization. + /// Default is "true"; set this to "false" to allow for manual startup though the + /// method. + /// + /// true if autostartup; otherwise, false. + public bool AutoStartup + { + set { autoStartup = value; } + } + + /// + /// Gets a value indicating whether this Container is active, + /// that is, whether it has been set up but not shut down yet. + /// + /// true if active; otherwise, false. + public bool Active + { + get + { + lock (lifecycleMonitor) + { + return active; + } + } + } + + + /// + /// Gets a value indicating whether this Container is running, + /// that is whether it has been started and not stopped yet. + /// + /// true if running; otherwise, false. + public bool Running + { + get + { + lock (lifecycleMonitor) + { + return (running && RunningAllowed()); + } + } + } + + #region IObjectNameAware Members + + /// + /// Return the object name that this listener container has been assigned + /// in its containing object factory, if any. + /// + public string ObjectName + { + set { objectName = value; } + get { return objectName; } + } + + #endregion + + #endregion + + #region IInitializingObject Members + + /// + /// Delegates to and + /// + public void AfterPropertiesSet() + { + ValidateConfiguration(); + Initialize(); + } + + #endregion + + /// + /// Validates the configuration of this container + /// The default implementation is empty. To be overridden in subclasses. + /// + protected virtual void ValidateConfiguration() + { + } + + #region IDisposable Members + + /// + /// Calls when the application context destroys the container instance. + /// + public void Dispose() + { + Shutdown(); + } + + #endregion + + public virtual void Initialize() + { + lock (lifecycleMonitor) + { + active = true; + Monitor.PulseAll(lifecycleMonitor); + } + if (autoStartup) + { + DoStart(); + } + DoInitialize(); + } + + public virtual void Shutdown() + { + LOG.Debug("Shutting down MessageListenerContainer"); + lock (lifecycleMonitor) + { + running = false; + active = false; + Monitor.PulseAll(lifecycleMonitor); + } + DoShutdown(); + } + + public virtual void Start() + { + DoStart(); + } + + protected virtual void DoStart() + { + lock (lifecycleMonitor) + { + running = true; + Monitor.PulseAll(lifecycleMonitor); + } + } + + public virtual void Stop() + { + DoStop(); + } + + public virtual void DoStop() + { + lock (lifecycleMonitor) + { + running = false; + Monitor.PulseAll(lifecycleMonitor); + } + } + + /// + /// Check whether this container's listeners are generally allowed to run. + /// + /// + /// This implementation always returns true; the default 'running' + /// state is purely determined by and + /// + /// Subclasses may override this method to check against temporary + /// conditions that prevent listeners from actually running. In other words, + /// they may apply further restrictions to the 'running' state, returning + /// false if such a restriction prevents listeners from running. + /// + /// + /// false if such a restriction prevents listeners from running. + protected virtual bool RunningAllowed() + { + return true; + } + + #region Abstract Methods + + /// + /// Subclasses need to implement this method for their specific + /// listener management process. + /// + protected abstract void DoInitialize(); + + /// + /// Subclasses need to implement this method for their specific + /// listener management process. + /// + protected abstract void DoShutdown(); + + #endregion + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/AbstractMessageListenerContainer.cs b/src/Spring/Spring.Messaging/Messaging/Listener/AbstractMessageListenerContainer.cs new file mode 100644 index 00000000..aa861c77 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/AbstractMessageListenerContainer.cs @@ -0,0 +1,197 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Messaging; +using Common.Logging; +using Spring.Context; +using Spring.Messaging.Core; +using Spring.Messaging.Support.Converters; +using Spring.Util; + +namespace Spring.Messaging.Listener +{ + /// + /// Defines a minimal programming model for message listener containers. They are expected to + /// invoke a upon asynchronous receives of a MSMQ message. Access to + /// obtain MessageQueue and instances is available through the + /// property, the default implementation + /// provides per-thread instances of these classes. + /// + /// Mark Pollack + public abstract class AbstractMessageListenerContainer : AbstractListenerContainer, IApplicationContextAware + { + #region Logging Definition + + private static readonly ILog LOG = LogManager.GetLogger(typeof (AbstractMessageListenerContainer)); + + #endregion + + #region Fields + + private string messageQueueObjectName; + + private IMessageQueueFactory messageQueueFactory; + private IApplicationContext applicationContext; + + /// + /// Most operations within the MessageListener container hierarchy use methods on the + /// MessageQueue instance which are thread safe (BeginPeek, BeginReceive, + /// EndPeek, EndReceive, GetAllMessages, Peek, and Receive). When using another + /// method on the shared MessageQueue instance, wrap calls with a lock on this object. + /// + protected object messageQueueMonitor = new object(); + + private IMessageListener messageListener; + + private TimeSpan recoveryTimeSpan = new TimeSpan(0, 0, 0, 1, 0); + + #endregion + + #region Properties + + /// + /// Gets or sets the message queue factory. + /// + /// The message queue factory. + public IMessageQueueFactory MessageQueueFactory + { + get { return messageQueueFactory; } + set { messageQueueFactory = value; } + } + + /// + /// Gets or sets the name of the message queue object, as refered to in the + /// Spring configuration, that will be used to create a DefaultMessageQueue instance + /// for consuming messages in the container. + /// + /// The name of the message queue object. + public string MessageQueueObjectName + { + get { return messageQueueObjectName; } + set + { + AssertUtils.ArgumentNotNull(value, "MessageQueueObjectName"); + messageQueueObjectName = value; + } + } + + /// + /// Gets or sets the message listener. + /// + /// The message listener. + public IMessageListener MessageListener + { + get { return messageListener; } + set + { + AssertUtils.ArgumentNotNull(value, "MessageListener"); + messageListener = value; + } + } + + /// + /// Gets or sets the recovery time span, how long to sleep after an exception in processing occured + /// to avoid excessive redelivery attempts. Default value is 1 second. + /// + /// The recovery time span. + public TimeSpan RecoveryTimeSpan + { + get { return recoveryTimeSpan; } + set { recoveryTimeSpan = value; } + } + + #endregion + + #region IApplicationContextAware Members + + public IApplicationContext ApplicationContext + { + get { return applicationContext; } + set { applicationContext = value; } + } + + #endregion + + #region Protected Methods + + protected override void ValidateConfiguration() + { + if (MessageQueueObjectName == null) + { + throw new ArgumentException("Property 'DefaultMessageQueueObjectName' is required"); + } + if (MessageQueueFactory == null) + { + DefaultMessageQueueFactory qf = new DefaultMessageQueueFactory(); + qf.ApplicationContext = applicationContext; + MessageQueueFactory = qf; + } + } + + + protected virtual void DoExecuteListener(Message message) + { + if (!Running) + { + if (LOG.IsWarnEnabled) + { + LOG.Warn("Not processing recieved message because of the listener container " + + "having been stopped in the meantime: " + message); + } + } + + InvokeListener(message); + } + + protected virtual void InvokeListener(Message message) + { + if (MessageListener != null) + { + DoInvokeListener(MessageListener, message); + } + else + { + throw new InvalidOperationException("No message listener specified - see property 'MessageListener'"); + } + } + + protected virtual void DoInvokeListener(IMessageListener listener, Message message) + { + listener.OnMessage(message); + } + + /// + /// Closes the queue handle. Cancel pending receive operation by closing the queue handle + /// To dispose of the queue handle, set EnableConnectionCache=false and call Close/Dispose. + /// + protected void CloseQueueHandle(MessageQueue mq) + { + lock (messageQueueMonitor) + { + MessageQueue.EnableConnectionCache = false; + mq.Close(); + mq.Dispose(); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/AbstractPeekingMessageListenerContainer.cs b/src/Spring/Spring.Messaging/Messaging/Listener/AbstractPeekingMessageListenerContainer.cs new file mode 100644 index 00000000..7c712694 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/AbstractPeekingMessageListenerContainer.cs @@ -0,0 +1,440 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Messaging; +using System.Threading; +using Common.Logging; + +namespace Spring.Messaging.Listener +{ + /// + /// Base class for listener container implementations which are based on Peeking for messages on + /// a MessageQueue. Peeking is the only resource efficient approach that can be used in + /// order to have MessageQueue receipt in conjunction with transactions, either local MSMQ transactions, + /// local ADO.NET based transactions, or DTC transactions. See SimpleMessageListenerContainer for + /// an implementation based on a synchronous receives and you do not require transactional support. + /// + /// + /// The number of threads that will be created for processing messages after the Peek occurs + /// is set via the property MaxConcurrentListeners. Each processing thread will continue to listen + /// for messages up until the the timeout value specified by ListenerTimeLimit or until + /// there are no more messages on the queue (which ver comes first). + /// + /// The default value of + /// ListenerTimeLimit is TimeSpan.Zero, meaning that only one attempt to recieve a message from the + /// queue will be performed by each listener thread. + /// + /// + /// The current implementation uses the standard .NET thread pool. Future implementations will + /// use a custom (and pluggable) thread pool. + /// + /// + public abstract class AbstractPeekingMessageListenerContainer : AbstractMessageListenerContainer + { + #region Logging Definition + + private static readonly ILog LOG = LogManager.GetLogger(typeof (AbstractPeekingMessageListenerContainer)); + + #endregion + + #region Fields + + private Thread dispatcherThread; + + private ManualResetEvent stopEvent = new ManualResetEvent(false); + + private MessageQueue messageQueue; + + private int maxConcurrentListeners = 1; + private bool setMaxConcurrentListenersCalled = false; + + private int activeListenerCount; + private int scheduledListenerCount; + private object activeListenerMonitor = new object(); + + private TimeSpan listenerTimeLimit = TimeSpan.Zero; + + #endregion + + #region Properties + + /// + /// Gets or sets the listener time limit to continuously receive messages. + /// The value is specified in milliseconds. The default value is TimeSpan.Zero, + /// indicating to only perform one Receive operation per Peek trigger. + /// + /// The listener time limit in millis. + public TimeSpan ListenerTimeLimit + { + get { return listenerTimeLimit; } + set { listenerTimeLimit = value; } + } + + /// + /// Gets or sets the max concurrent listeners to receive messages. + /// + /// The max concurrent listeners. + public int MaxConcurrentListeners + { + get { return maxConcurrentListeners; } + set + { + if (!setMaxConcurrentListenersCalled) + { + setMaxConcurrentListenersCalled = true; + maxConcurrentListeners = value; + } + else + { + LOG.Info("Ignoring resetting of MaxConcurrentListeners. Using previous value of " + + maxConcurrentListeners); + } + } + } + + /// + /// Gets or sets the message queue used for Peeking. + /// + /// The message queue. + public MessageQueue MessageQueue + { + get { return messageQueue; } + } + + #endregion + + #region Protected Container Lifecycle Methods + + /// + /// Retrieves a MessageQueue instance given the MessageQueueObjectName + /// + protected override void DoInitialize() + { + messageQueue = MessageQueueFactory.CreateMessageQueue(MessageQueueObjectName); + //TODO would initialize resources for a seperate thread pool here. + } + + /// + /// Wait for all listener threads to exit and closes the DefaultMessageQueue. + /// + /// + protected override void DoShutdown() + { + WaitForListenerThreadsToExit(); + CloseQueueHandle(MessageQueue); + if (dispatcherThread != null) + { + LOG.Debug("Waiting to join dispatcher thread."); + dispatcherThread.Join(); + dispatcherThread = null; + LOG.Debug("Dispatcher thread terminated."); + } + } + + /// + /// Starts peeking on the DefaultMessageQueue. + /// + protected override void DoStart() + { + base.DoStart(); + stopEvent = new ManualResetEvent(false); + dispatcherThread = new Thread(new ThreadStart(StartPeeking)); + ConfigureInitialPeekThread(dispatcherThread); + dispatcherThread.Start(); + } + + /// + /// Stops peeking on the message queue. + /// + public override void DoStop() + { + base.DoStop(); + stopEvent.Set(); + CloseQueueHandle(MessageQueue); + if (dispatcherThread != null) + { + LOG.Debug("Waiting to join dispatcher thread."); + dispatcherThread.Join(); + dispatcherThread = null; + LOG.Debug("Dispatcher thread terminated."); + } + } + + #endregion + + #region Protected Methods + + /// + /// Starts peeking on the DefaultMessageQueue. This is the method that must be called + /// again at the end of message procesing to continue the peeking process. + /// + protected virtual void StartPeeking() + { + if (Running) + { + try + { + IAsyncResult asynchResult = MessageQueue.BeginPeek(); + LOG.Debug("Waiting on Peek AsyncWaitHandle"); + int firedWaitHandle = WaitHandle.WaitAny(new WaitHandle[] {asynchResult.AsyncWaitHandle, stopEvent}); + if (firedWaitHandle == 0) + { + PeekCompleted(asynchResult); + } + else + { + //Stopping processing. + return; + } + } + catch (Exception ex) + { + LOG.Error( + "Exception executing DefaultMessageQueue.BeginPeek. Reinvoking after recovery interval [" + + RecoveryTimeSpan + "]", ex); + Thread.Sleep(RecoveryTimeSpan); + StartPeeking(); + } + } + } + + /// + /// The callback when the peek has completed. Schedule up to the maximum number of + /// concurrent listeners to receive messages off the queue. Delegates to the abstract + /// method DoReceiveAndExecute so that subclasses may customize the receiving process, + /// for example to surround the receive operation with transactional semantics. + /// + /// The async result. + protected virtual void PeekCompleted(IAsyncResult asyncResult) + { + bool listenerThreadWillCallStartPeek = false; + try + { + LOG.Debug("Peek Completed called."); + + MessageQueue.EndPeek(asyncResult); + + int numberOfListenersToSchedule = 0; + + // lock also prevents listeners that are about to exit from invoking + // StartPeeking while new listeners are being scheduled. + lock (activeListenerMonitor) + { + numberOfListenersToSchedule = maxConcurrentListeners - + (activeListenerCount + scheduledListenerCount); + + LOG.Debug("Submitting " + numberOfListenersToSchedule + " listener work items"); + + #region Submit to thread pool up to max number of concurrent listeners + + for (int i = 1; i <= numberOfListenersToSchedule; i++) + { + bool wasQueued = ThreadPool.QueueUserWorkItem(new WaitCallback(ReceiveAndExecute), MessageQueue); + if (wasQueued) + { + scheduledListenerCount++; + listenerThreadWillCallStartPeek = true; + LOG.Debug("Queued ReceiveAndExecute listener # " + i); + } + else + { + LOG.Error("Could not submit ReceiveAndExecute work item for listener # " + i); + } + } + Monitor.PulseAll(activeListenerMonitor); + } + + #endregion + } + catch (MessageQueueException mex) + { + switch ((int) mex.MessageQueueErrorCode) + { + case -1073741536: // = 0xc0000120 "STATUS_CANCELLED". + LOG.Info("Asynchronous Peek Thread sent STATUS_CANCELLED."); + break; + default: + LOG.Error("MessageQueueException Peeking Message", mex); + break; + } + } + catch (Exception e) + { + LOG.Error("Exception Peeking Message", e); + } + finally + { + if (listenerThreadWillCallStartPeek == false && Running) + { + LOG.Warn( + "Could not queue any listeners onto the thread pool. Calling BeginPeek again after delay of " + + RecoveryTimeSpan); + Thread.Sleep(RecoveryTimeSpan); + StartPeeking(); + } + } + } + + /// + /// Execute the listener for a message received from the given queue + /// wrapping the entire operation in an external transaction if demanded. + /// + /// The DefaultMessageQueue upon which the call to receive should be + /// called. + protected virtual void ReceiveAndExecute(object state) + { + bool messageRecieved = true; + bool listenerTimeOut = false; + + MessageQueue mq = state as MessageQueue; + if (mq == null) + { + throw new ArgumentException("Expected asynchronous state object to be of the type DefaultMessageQueue"); + } + + try + { + LOG.Debug("Executing ReceiveAndExecute"); + + #region Increment Active Listener Count + + lock (activeListenerMonitor) + { + activeListenerCount++; + scheduledListenerCount--; + LOG.Debug("ActiveListenerCount = " + activeListenerCount); + LOG.Debug("ScheduledListenerCount = " + scheduledListenerCount); + Monitor.PulseAll(activeListenerMonitor); + } + + #endregion + + DateTime expirationTime = DateTime.Now.Add(ListenerTimeLimit); + while (!listenerTimeOut && messageRecieved) + { + //Subclasses to perform receive operation + messageRecieved = DoReceiveAndExecute(mq); + + if (ListenerTimeLimit == TimeSpan.Zero) + { + listenerTimeOut = true; + LOG.Trace("No listener timelimit specified, exiting recieve loop after one iteration."); + } + else if (DateTime.Now >= expirationTime) + { + listenerTimeOut = true; + LOG.Trace("Listener timeout, exiting receive loop."); + } + else + { + LOG.Trace("Continuing receive loop."); + } + } + } + catch (Exception ex) + { + messageRecieved = false; + LOG.Error("Error receiving message from DefaultMessageQueue = [" + mq.QueueName + "]", ex); + } + finally + { + LOG.Debug("Exiting ReceiveAndExecute"); + + #region Decrementing Listener Count and call StartPeeking if last listener or there are still messages to process + + lock (activeListenerMonitor) + { + activeListenerCount--; + LOG.Debug("ActiveListenerCount = " + activeListenerCount); + LOG.Trace("ListenerTimeout = " + listenerTimeOut + ", MessageRecieved = " + messageRecieved); + if (activeListenerCount == 0) + { + LOG.Debug("All processing threads ended - calling StartPeek again."); + //last active worker thread needs to restart the peeking process + StartPeeking(); + } + else if (listenerTimeOut && messageRecieved) + { + LOG.Debug( + "Processing thread ended due to timeout and last recieve operation was successfull, calling StartPeek again."); + StartPeeking(); + } + Monitor.PulseAll(activeListenerMonitor); + } + + #endregion + } + } + + /// + /// Subclasses perform a receive opertion on the message queue and execute the + /// message listener + /// + /// The DefaultMessageQueue. + /// true if received a message, false otherwise + protected abstract bool DoReceiveAndExecute(MessageQueue mq); + + + /// + /// Waits for listener threads to exit. + /// + protected virtual void WaitForListenerThreadsToExit() + { + try + { + lock (activeListenerMonitor) + { + if (activeListenerCount > 0) + { + while (activeListenerCount > 0) + { + LOG.Debug("Waiting for termination of " + activeListenerCount + " listener threads."); + Monitor.Wait(activeListenerMonitor); + } + } + } + } + catch (ThreadInterruptedException) + { + Thread.CurrentThread.Interrupt(); + } + } + + + protected virtual void ConfigureInitialPeekThread(Thread thread) + { + thread.IsBackground = true; + } + + + /// + /// Template method that gets called right when a new message has been received, + /// before attempting to process it. Allows subclasses to react to the event + /// of an actual incoming message, for example adapting their consumer count. + /// + /// The message. + protected virtual void MessageReceived(Message message) + { + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/AbstractSendToQueueExceptionHandler.cs b/src/Spring/Spring.Messaging/Messaging/Listener/AbstractSendToQueueExceptionHandler.cs new file mode 100644 index 00000000..571c6cb7 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/AbstractSendToQueueExceptionHandler.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections; +using System.Messaging; +using Spring.Context; +using Spring.Messaging.Core; +using Spring.Objects.Factory; + +namespace Spring.Messaging.Listener +{ + public class AbstractSendToQueueExceptionHandler : IInitializingObject, IApplicationContextAware + { + private int maxRetry = 5; + + private IMessageQueueFactory messageQueueFactory; + private string messageQueueObjectName; + private IApplicationContext applicationContext; + + + protected object messageMapMonitor = new object(); + protected IDictionary messageMap = new Hashtable(); + + /// + /// Gets or sets the maximum retry count to reattempt processing of a message that has thrown + /// an exception + /// + /// The max retry count. + public int MaxRetry + { + get { return maxRetry; } + set { maxRetry = value; } + } + + /// + /// Gets or sets the message queue factory. + /// + /// The message queue factory. + public IMessageQueueFactory MessageQueueFactory + { + get { return messageQueueFactory; } + set { messageQueueFactory = value; } + } + + /// + /// Gets or sets the name of the message queue object to send the message that cannot be + /// processed successfully after MaxRetry delivery attempts. + /// + /// The name of the message queue object. + public string MessageQueueObjectName + { + get { return messageQueueObjectName; } + set { messageQueueObjectName = value; } + } + + #region IApplicationContextAware Members + + /// + /// Set the that this + /// object runs in. + /// + public IApplicationContext ApplicationContext + { + get { return applicationContext; } + set { applicationContext = value; } + } + + #endregion + + #region IInitializingObject Members + + /// + /// Ensure that the MessageQueueObject name is set and creates a + /// if no + /// is specified. + /// + /// Will attempt to create an instance of the DefaultMessageQueue to detect early + /// any configuraiton errors. + /// + /// In the event of misconfiguration (such as the failure to set a + /// required property) or if initialization fails. + /// + public virtual void AfterPropertiesSet() + { + if (MessageQueueObjectName == null) + { + throw new ArgumentException("The DefaultMessageQueueObjectName property has not been set."); + } + if (messageQueueFactory == null) + { + DefaultMessageQueueFactory mqf = new DefaultMessageQueueFactory(); + mqf.ApplicationContext = applicationContext; + messageQueueFactory = mqf; + } + //Create an instance so we can 'fail-fast' if there isn't an DefaultMessageQueue unde + MessageQueue mq = MessageQueueFactory.CreateMessageQueue(messageQueueObjectName); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/AbstractTransactionalMessageListenerContainer.cs b/src/Spring/Spring.Messaging/Messaging/Listener/AbstractTransactionalMessageListenerContainer.cs new file mode 100644 index 00000000..a1f49957 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/AbstractTransactionalMessageListenerContainer.cs @@ -0,0 +1,120 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Messaging; +using System.Threading; +using Common.Logging; +using Spring.Data.Core; +using Spring.Messaging.Core; +using Spring.Transaction; +using Spring.Transaction.Support; + +namespace Spring.Messaging.Listener +{ + /// + /// An implementation of a Peeking based MessageListener container that starts a transaction + /// before recieving a message. The implementation determines + /// the type of transaction that will be started. An exception while processing the message will + /// result in a rollback, otherwise a transaction commit will be performed. + /// + /// + /// The type of transaction that can be started can either be local transaction, + /// (e.g. , a local messaging transaction + /// (e.g. or a DTC based transaction, + /// (eg. . + /// + /// Transaction properties can be set using the property + /// and the transaction timeout via the property . + /// + /// + public abstract class AbstractTransactionalMessageListenerContainer : AbstractPeekingMessageListenerContainer + { + #region Logging Definition + + private static readonly ILog LOG = LogManager.GetLogger(typeof (AbstractTransactionalMessageListenerContainer)); + + #endregion + + private IPlatformTransactionManager platformTransactionManager; + + private DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition(); + + + public IPlatformTransactionManager PlatformTransactionManager + { + get { return platformTransactionManager; } + set { platformTransactionManager = value; } + } + + public DefaultTransactionDefinition TransactionDefinition + { + get { return transactionDefinition; } + set { transactionDefinition = value; } + } + + /// + /// Sets the transaction timeout to use for transactional wrapping, in seconds. + /// Default is none, using the transaction manager's default timeout. + /// + /// The transaction timeout. + public int TransactionTimeout + { + set { transactionDefinition.TransactionTimeout = value; } + } + + protected override bool DoReceiveAndExecute(MessageQueue mq) + { + bool messageReceived = false; + // Execute receive within transaction. + ITransactionStatus status = PlatformTransactionManager.GetTransaction(TransactionDefinition); + try + { + messageReceived = DoReceiveAndExecuteUsingPlatformTransactionManager(mq, status); + } + catch (Exception ex) + { + RollbackOnException(status, ex); + Thread.Sleep(RecoveryTimeSpan); + throw; + } + //if status has indicated rollback only, will rollback. + PlatformTransactionManager.Commit(status); + return messageReceived; + } + + protected abstract bool DoReceiveAndExecuteUsingPlatformTransactionManager(MessageQueue mq, + ITransactionStatus status); + + protected void RollbackOnException(ITransactionStatus status, Exception ex) + { + LOG.Debug("Initiating transaction rollback on listener exception", ex); + try + { + PlatformTransactionManager.Rollback(status); + } + catch (Exception ex2) + { + LOG.Error("Listener exception overridden by rollback error", ex2); + throw; + } + } + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/DistributedTxMessageListenerContainer.cs b/src/Spring/Spring.Messaging/Messaging/Listener/DistributedTxMessageListenerContainer.cs new file mode 100644 index 00000000..099b832a --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/DistributedTxMessageListenerContainer.cs @@ -0,0 +1,198 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Messaging; +using System.Transactions; +using Common.Logging; +using Spring.Transaction; + +namespace Spring.Messaging.Listener +{ + /// + /// A MessageListenerContainer that uses distributed (DTC) based transactions. Exceptions are + /// handled by instances of . + /// + /// + /// + /// Starts a DTC based transaction before receiving the message. The transaction is + /// automaticaly promoted to 2PC to avoid the default behaivor of transactional promotion. + /// Database and messaging operations will commit or rollback together. + /// + /// + /// If you only want local message based transactions use the + /// . With some simple programming + /// you may also achieve 'exactly once' processing using the + /// . + /// + /// + /// Poison messages can be detected and sent to another queue using Spring's + /// . + /// + /// + public class DistributedTxMessageListenerContainer : AbstractTransactionalMessageListenerContainer + { + #region Logging Definition + + private static readonly ILog LOG = LogManager.GetLogger(typeof (DistributedTxMessageListenerContainer)); + + #endregion + + private IDistributedTransactionExceptionHandler distributedTransactionExceptionHandler; + + + /// + /// Gets or sets the distributed transaction exception handler. + /// + /// The distributed transaction exception handler. + public IDistributedTransactionExceptionHandler DistributedTransactionExceptionHandler + { + get { return distributedTransactionExceptionHandler; } + set { distributedTransactionExceptionHandler = value; } + } + + /// + /// Set the transaction name to be the spring object name. + /// Call base class Initialize() functionality. + /// + public override void Initialize() + { + // Use object name as default transaction name. + if (TransactionDefinition.Name == null) + { + TransactionDefinition.Name = ObjectName; + } + + // Proceed with superclass initialization. + base.Initialize(); + } + + protected override bool DoReceiveAndExecuteUsingPlatformTransactionManager(MessageQueue mq, + ITransactionStatus status) + { + #region Logging + + if (LOG.IsDebugEnabled) + { + LOG.Debug("Executing DoReceiveAndExecuteUsingTxScopeTransactionManager"); + } + + #endregion Logging + + //We are sure to be talking to a second resource manager, so avoid going through + //the promotable transaction and force a distributed transaction right from the start. + TransactionInterop.GetTransmitterPropagationToken(System.Transactions.Transaction.Current); + + Message message; + try + { + message = mq.Receive(TimeSpan.Zero, MessageQueueTransactionType.Automatic); + } + catch (MessageQueueException ex) + { + if (ex.MessageQueueErrorCode != MessageQueueErrorCode.IOTimeout) + { + //expected to occur occasionally + + #region Logging + + if (LOG.IsTraceEnabled) + { + LOG.Trace( + "MessageQueueErrorCode.IOTimeout: No message available to receive. May have been processed by another thread."); + } + + #endregion + + status.RollbackOnly = true; + return false; // no more peeking unless this is the last listener thread + } + else + { + // A real issue in receiving the message + lock (messageQueueMonitor) + { + mq.Close(); + MessageQueue.ClearConnectionCache(); + } + throw; // will cause rollback in surrounding platform transaction manager and log exception + } + } + + if (message == null) + { + #region Logging + + if (LOG.IsTraceEnabled) + { + LOG.Trace("Message recieved is null from Queue = [" + mq.QueueName + "]"); + } + + #endregion + + status.RollbackOnly = true; + return false; // no more peeking unless this is the last listener thread + } + + + try + { + #region Logging + + if (LOG.IsDebugEnabled) + { + LOG.Debug("Received message [" + message.Id + "] on queue [" + mq.QueueName + "]"); + } + + #endregion + + MessageReceived(message); + if (DistributedTransactionExceptionHandler != null) + { + if (DistributedTransactionExceptionHandler.IsPoisonMessage(message)) + { + DistributedTransactionExceptionHandler.HandlePoisonMessage(message); + return true; // will remove from queue and continue receive loop. + } + } + DoExecuteListener(message); + } + catch (Exception ex) + { + HandleDistributedTransactionListenerException(ex, message); + throw; // will rollback and keep message on the queue. + } + finally + { + message.Dispose(); + } + return true; + } + + protected virtual void HandleDistributedTransactionListenerException(Exception exception, Message message) + { + IDistributedTransactionExceptionHandler exceptionHandler = DistributedTransactionExceptionHandler; + if (exceptionHandler != null) + { + exceptionHandler.OnException(exception, message); + } + } + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/IDistributedTransactionExceptionHandler.cs b/src/Spring/Spring.Messaging/Messaging/Listener/IDistributedTransactionExceptionHandler.cs new file mode 100644 index 00000000..7e5705ea --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/IDistributedTransactionExceptionHandler.cs @@ -0,0 +1,66 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Messaging; + +namespace Spring.Messaging.Listener +{ + /// + /// Exception handler for use with DTC based message listener container. + /// such as . + /// See for + /// an implementation that detects poison messages and send them to another queue. + /// + public interface IDistributedTransactionExceptionHandler + { + /// + /// Determines whether the incoming message is a poison message. This method is + /// called before the is invoked. + /// + /// + /// The will call + /// if this method returns true and will + /// then commit the distibuted transaction (removing the message from the queue). + /// + /// The incoming message. + /// + /// true if it is a poison message; otherwise, false. + /// + bool IsPoisonMessage(Message message); + + /// + /// Handles the poison message. + /// + /// Typical implementations will move the message to another queue. + /// The will call this + /// method while still within a DTC-based transaction. + /// + /// The poison message. + void HandlePoisonMessage(Message poisonMessage); + + /// + /// Called when an exception is thrown in listener processing. + /// + /// The exception. + /// The message. + void OnException(Exception exception, Message message); + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/IExceptionHandler.cs b/src/Spring/Spring.Messaging/Messaging/Listener/IExceptionHandler.cs new file mode 100644 index 00000000..fff6dd27 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/IExceptionHandler.cs @@ -0,0 +1,47 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Messaging; + +namespace Spring.Messaging.Listener +{ + /// + /// Exception handler called when an exception occurs during + /// non-transactional receive processing. + /// + /// + /// A non-transactional receive will remove the message from the queue. Non-transactional + /// receivers do not suffer from poison messages since there is no redelivery by MSMQ. + /// Typical actions to perform are to log the message or place it in another queue. + /// If placed in another queue, another message listener container can be used to + /// process the message later, assuming the root cause of the original exception is + /// transient in nature. + /// + public interface IExceptionHandler + { + /// + /// Called when an exception is thrown in listener processing. + /// + /// The exception. + /// The message. + void OnException(Exception exception, Message message); + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/IMessageListener.cs b/src/Spring/Spring.Messaging/Messaging/Listener/IMessageListener.cs new file mode 100644 index 00000000..c5a10d09 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/IMessageListener.cs @@ -0,0 +1,37 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System.Messaging; + +namespace Spring.Messaging.Listener +{ + /// + /// The callback invoked when a message is received. + /// + /// Mark Pollack + public interface IMessageListener + { + /// + /// Called when message is received. + /// + /// The message. + void OnMessage(Message message); + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/IMessageTransactionExceptionHandler.cs b/src/Spring/Spring.Messaging/Messaging/Listener/IMessageTransactionExceptionHandler.cs new file mode 100644 index 00000000..618eea14 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/IMessageTransactionExceptionHandler.cs @@ -0,0 +1,52 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Messaging; + +namespace Spring.Messaging.Listener +{ + /// + /// The exception handler within a transactional context. + /// + /// + /// The return value indicates to the invoker (typically a + /// ) whether the + /// MessageTransaction that is associated with the delivery of this message + /// should be rolled back (placing the message back on the transactional queue + /// for redelivery) or commited (removing the message from the transactional queue) + /// + /// Mark Pollack + public interface IMessageTransactionExceptionHandler + { + /// + /// Called when an exception is thrown during listener processing under the + /// scope of a . + /// + /// The exception. + /// The message. + /// The message queue transaction. + /// An action indicating if the caller should commit or rollback the + /// + /// + TransactionAction OnException(Exception exception, Message message, + MessageQueueTransaction messageQueueTransaction); + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/ListenerClassDiagram.cd b/src/Spring/Spring.Messaging/Messaging/Listener/ListenerClassDiagram.cd new file mode 100644 index 00000000..38a48bdd --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/ListenerClassDiagram.cd @@ -0,0 +1,55 @@ + + + + + + + Messaging\Listener\AbstractListenerContainer.cs + AAAAAAAgACgAAQAAEAEAIxAAAAgAAAYpIABAIAgAAAA= + + + + + + + Messaging\Listener\AbstractMessageListenerContainer.cs + AABAMYAgABgAABIAAAAIAACAAAAAAAAAAAEAAAhAAAA= + + + + + + + Messaging\Listener\AbstractPeekingMessageListenerContainer.cs + AAAAAAAiwAghGCBAABEAAhSEAABAQAQIAAAAAIAAAAA= + + + + + + Messaging\Listener\DistributedTxMessageListenerContainer.cs + AAABAAAAAAgAAQAAAAAIAAAAAAQAAAAAAAAAAAAAAQA= + + + + + + Messaging\Listener\AbstractTransactionalMessageListenerContainer.cs + AAAAAIAAAAggEACAAAAIAAAAEAAAAAAAAAAAAAAAAAI= + + + + + + Messaging\Listener\TransactionalMessageListenerContainer.cs + AAhAAAAQAAgAASAIAAAKAAAAAAEAAgAAAAQAAACAAAg= + + + + + + Messaging\Listener\NonTransactionalMessageListenerContainer.cs + AAAAIAAAAAggAABAAAAAIAAAAAAAAAAAAAAAAAAAAAA= + + + \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/MessageListenerAdapter.cs b/src/Spring/Spring.Messaging/Messaging/Listener/MessageListenerAdapter.cs new file mode 100644 index 00000000..fe4ec74e --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/MessageListenerAdapter.cs @@ -0,0 +1,362 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Collections; +using System.Messaging; +using Common.Logging; +using Spring.Context; +using Spring.Expressions; +using Spring.Messaging.Core; +using Spring.Messaging.Support; +using Spring.Messaging.Support.Converters; +using Spring.Objects.Factory; +using Spring.Objects.Factory.Support; +using Spring.Reflection.Dynamic; + +namespace Spring.Messaging.Listener +{ + /// + /// Message listener adapter that delegates the handling of messages to target + /// listener methods via reflection , + /// with flexible message type conversion. + /// Allows listener methods to operate on message content types, completely + /// independent from the MSMQ API. + /// + /// + /// + /// By default, the content of incoming MSMQ messages gets extracted before + /// being passed into the target handler method, to let the target method + /// operate on message content types such as String or business object instead of + /// . Message type conversion is delegated to a Spring + /// By default, an + /// with TargetType set to System.String is used. If you do not want such automatic + /// message conversion taking place, then be sure to set the + /// MessageConverter property to null. + /// + /// + /// If a target handler method returns a non-null object (for example, with a + /// message content type such as String), it will get + /// wrapped in a MSMQ Message and sent to the response destination + /// (either using the MSMQ Message.ResponseQueue property or + /// ) specified default response queue + /// destination). + /// + /// + /// Find below some examples of method signatures compliant with this adapter class. + /// This first example uses the default that can + /// marhsall/unmarshall string values from the MSMQ Message. + /// + /// + /// public interface IMyHandler + /// { + /// void HandleMessage(string text); + /// } + /// + /// + /// The next example indicates a similar method signature but the name of the + /// handler method name has been changed to "DoWork", using the property + /// + /// + /// + /// public interface IMyHandler + /// { + /// void DoWork(string text); + /// } + /// + /// If your implementation will return multiple object + /// types, overloading the handler method is perfectly acceptible, the most specific matching + /// method will be used. A method with an object signature would be consider a + /// 'catch-all' method + /// + /// + /// public interface IMyHandler + /// { + /// void DoWork(string text); + /// void DoWork(OrderRequest orderRequest); + /// void DoWork(InvoiceRequest invoiceRequest); + /// void DoWork(object obj); + /// } + /// + /// + /// The last example shows how to send a message to the ResponseQueue for those + /// methods that do not return void. + /// + /// public interface MyHandler + /// { + /// string DoWork(string text); + /// OrderResponse DoWork(OrderRequest orderRequest); + /// InvoiceResponse DoWork(InvoiceRequest invoiceRequest); + /// void DoWork(object obj); + /// } + /// + /// + /// + /// Mark Pollack + public class MessageListenerAdapter : IMessageListener, IApplicationContextAware, IInitializingObject + { + #region Logging + + private static readonly ILog logger = LogManager.GetLogger(typeof (MessageListenerAdapter)); + + #endregion + + private IApplicationContext applicationContext; + + private object handlerObject; + + private string defaultHandlerMethod = "HandleMessage"; + + private IExpression processingExpression; + + private string defaultResponseQueueName; + + private string messageConverterObjectName; + + private MessageQueueTemplate messageQueueTemplate; + + private IMessageQueueFactory messageQueueFactory; + + + public MessageListenerAdapter() + { + handlerObject = this; + processingExpression = Expression.Parse(defaultHandlerMethod + "(#convertedObject)"); + messageQueueTemplate = new MessageQueueTemplate(); + } + + public MessageListenerAdapter(object handlerObject) + { + this.handlerObject = handlerObject; + } + + public object HandlerObject + { + get { return handlerObject; } + set { handlerObject = value; } + } + + public string DefaultHandlerMethod + { + get { return defaultHandlerMethod; } + set { defaultHandlerMethod = value; } + } + + #region IInitializingObject Members + + public void AfterPropertiesSet() + { + if (messageQueueFactory == null) + { + DefaultMessageQueueFactory mqf = new DefaultMessageQueueFactory(); + mqf.ApplicationContext = applicationContext; + messageQueueFactory = mqf; + } + if (messageConverterObjectName == null) + { + messageConverterObjectName = QueueUtils.RegisterDefaultMessageConverter(applicationContext); + } + } + + #endregion + + #region IApplicationContextAware Members + + public IApplicationContext ApplicationContext + { + get { return applicationContext; } + set { applicationContext = value; } + } + + #endregion + + public IMessageQueueFactory MessageQueueFactory + { + get { return messageQueueFactory; } + set { messageQueueFactory = value; } + } + + public string DefaultResponseQueueName + { + get { return defaultResponseQueueName; } + set { defaultResponseQueueName = value; } + } + + public MessageQueue DefaultResponseQueue + { + get + { + if (DefaultResponseQueueName != null) + { + return messageQueueFactory.CreateMessageQueue(DefaultResponseQueueName); + } + else + { + return null; + } + /* + DefaultMessageQueue mq = LogicalThreadContext.GetData(CURRENT_RESPONSEQUEUE_SLOTNAME) as DefaultMessageQueue; + if (mq == null) + { + mq = ApplicationContext.GetObject(DefaultResponseQueueName) as DefaultMessageQueue; + LogicalThreadContext.SetData(CURRENT_RESPONSEQUEUE_SLOTNAME, mq); + } + return mq; + */ + } + } + + + public string MessageConverterObjectName + { + get { return messageConverterObjectName; } + set { messageConverterObjectName = value; } + } + + public IMessageConverter MessageConverter + { + get + { + return messageQueueFactory.CreateMessageConverter(MessageConverterObjectName); + /* + if (messageConverter == null) + { + throw new InvalidOperationException("No MessageConverter registered. Check configuration of MessageQueueTemplate."); + } + IMessageConverter mc = LogicalThreadContext.GetData(CURRENT_CONVERTER_SLOTNAME) as IMessageConverter; + if (mc == null) + { + mc = messageConverter.Clone() as IMessageConverter; + LogicalThreadContext.SetData(CURRENT_CONVERTER_SLOTNAME, mc); + } + return mc;*/ + } + } + + + protected virtual string GetListenerMethodName(Message originalMessage, object extractedMessage) + { + return DefaultHandlerMethod; + } + + #region IMessageListener Members + + public virtual void OnMessage(Message message) + { + object convertedMessage = ExtractMessage(message); + + IDictionary vars = new Hashtable(); + vars["convertedObject"] = convertedMessage; + + //Need to parse each time since have overloaded methods and + //expression processor caches target of first invocation. + //TODO - use regular reflection. + processingExpression = Expression.Parse(defaultHandlerMethod + "(#convertedObject)"); + + //Invoke message handler method and get result. + object result = processingExpression.GetValue(handlerObject, vars); + if (result != null) + { + HandleResult(result, message); + } + else + { + logger.Debug("No result object given - no result to handle"); + } + } + + #endregion + + protected virtual object ExtractMessage(Message message) + { + IMessageConverter converter = MessageConverter; + if (converter != null) + { + return converter.FromMessage(message); + } + return message; + } + + private void HandleResult(object result, Message request) + { + if (logger.IsDebugEnabled) + { + logger.Debug("Listener method returned result [" + result + + "] - generating response message for it"); + } + Message response = BuildMessage(result); + PostProcessResponse(request, response); + MessageQueue destination = GetResponseDestination(request, response); + SendResponse(destination, response); + } + + protected virtual void SendResponse(MessageQueue destination, Message response) + { + //Will send with appropriate transaction semantics + messageQueueTemplate.Send(destination, response); + } + + protected virtual Message BuildMessage(object result) + { + IMessageConverter converter = MessageConverter; + if (converter != null) + { + if (messageConverterObjectName.Equals("__XmlMessageConverter__")) + { + return converter.ToMessage(result.ToString()); + } + else + { + return converter.ToMessage(result); + } + } + else + { + Message msg = result as Message; + if (msg == null) + { + throw new MessagingException("No MessageConverter specified - cannot handle message [" + result + + "]"); + } + return msg; + } + } + + protected virtual void PostProcessResponse(Message request, Message response) + { + response.CorrelationId = request.CorrelationId; + } + + protected virtual MessageQueue GetResponseDestination(Message request, Message response) + { + MessageQueue replyTo = request.ResponseQueue; + if (replyTo == null) + { + replyTo = DefaultResponseQueue; + if (replyTo == null) + { + throw new MessagingException("Cannot determine response destination: " + + "Request message does not contain ResponseQueue destination, and no default response queue set."); + } + } + return replyTo; + } + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/NonTransactionalMessageListenerContainer.cs b/src/Spring/Spring.Messaging/Messaging/Listener/NonTransactionalMessageListenerContainer.cs new file mode 100644 index 00000000..e6b59f5e --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/NonTransactionalMessageListenerContainer.cs @@ -0,0 +1,158 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Messaging; +using Common.Logging; + +namespace Spring.Messaging.Listener +{ + /// + /// An implementation of a Peeking based MessageListener container that does not surround the + /// receive operation with a transaction. + /// + /// + /// Exceptions that occur during message processing are handled by an instance + /// of . + /// + public class NonTransactionalMessageListenerContainer : AbstractPeekingMessageListenerContainer + { + #region Logging Definition + + private static readonly ILog LOG = LogManager.GetLogger(typeof (NonTransactionalMessageListenerContainer)); + + #endregion + + private IExceptionHandler exceptionHandler; + + + public IExceptionHandler ExceptionHandler + { + get { return exceptionHandler; } + set { exceptionHandler = value; } + } + + + protected virtual void HandleListenerException(Exception e, Message message) + { + IExceptionHandler exceptionHandler = ExceptionHandler; + if (exceptionHandler != null) + { + exceptionHandler.OnException(e, message); + } + } + + protected override bool DoReceiveAndExecute(MessageQueue mq) + { + Message message = null; + try + { + #region Logging + + if (LOG.IsTraceEnabled) + { + LOG.Trace("Receiving message with zero timeout for queue = [" + mq.QueueName + "]"); + } + + #endregion + + message = mq.Receive(TimeSpan.Zero); + } + catch (MessageQueueException ex) + { + if (ex.MessageQueueErrorCode == MessageQueueErrorCode.IOTimeout) + { + //expected to occur occasionally + + #region Logging + + if (LOG.IsTraceEnabled) + { + LOG.Trace( + "MessageQueueErrorCode.IOTimeout: No message available to receive. May have been processed by another thread."); + } + + #endregion + + return false; // no more peeking unless this is the last listener thread + } + else + { + // A real issue in receiving the message + + #region Logging + + if (LOG.IsErrorEnabled) + { + LOG.Error("Error receiving message from DefaultMessageQueue [" + mq.QueueName + + "], closing queue and clearing connection cache."); + } + + #endregion + + lock (messageQueueMonitor) + { + mq.Close(); + MessageQueue.ClearConnectionCache(); + } + throw; // will log exception. + } + } + + if (message == null) + { + #region Logging + + if (LOG.IsTraceEnabled) + { + LOG.Trace("Message recieved is null from Queue = [" + mq.QueueName + "]"); + } + + #endregion + + return false; // no more peeking unless this is the last listener thread + } + + try + { + #region Logging + + if (LOG.IsDebugEnabled) + { + LOG.Debug("Received message [" + message.Id + "] on queue [" + mq.QueueName + "]"); + } + + #endregion + + MessageReceived(message); + DoExecuteListener(message); + } + catch (Exception ex) + { + HandleListenerException(ex, message); + } + finally + { + message.Dispose(); + } + return true; + } + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/SendToQueueDistributedTransactionExceptionHandler.cs b/src/Spring/Spring.Messaging/Messaging/Listener/SendToQueueDistributedTransactionExceptionHandler.cs new file mode 100644 index 00000000..12ea15b8 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/SendToQueueDistributedTransactionExceptionHandler.cs @@ -0,0 +1,117 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + + +using System; +using System.Messaging; +using Common.Logging; + +namespace Spring.Messaging.Listener +{ + public class SendToQueueDistributedTransactionExceptionHandler : AbstractSendToQueueExceptionHandler, + IDistributedTransactionExceptionHandler + { + #region Logging Definition + + private static readonly ILog LOG = + LogManager.GetLogger(typeof (SendToQueueDistributedTransactionExceptionHandler)); + + #endregion + + #region IDistributedTransactionExceptionHandler Members + + public bool IsPoisonMessage(Message message) + { + string messageId = message.Id; + lock (messageMapMonitor) + { + MessageStats messageStats = null; + if (messageMap.Contains(messageId)) + { + messageStats = (MessageStats) messageMap[messageId]; + if (messageStats.Count > MaxRetry) + { + LOG.Warn("Message with id = [" + message.Id + "] detected as poison message."); + return true; + } + } + return false; + } + } + + public void HandlePoisonMessage(Message message) + { + SendMessageToQueue(message); + } + + public void OnException(Exception exception, Message message) + { + string messageId = message.Id; + lock (messageMapMonitor) + { + MessageStats messageStats = null; + if (messageMap.Contains(messageId)) + { + messageStats = (MessageStats) messageMap[messageId]; + } + else + { + messageStats = new MessageStats(); + messageMap[messageId] = messageStats; + } + messageStats.Count++; + LOG.Warn("Message Error Count = [" + messageStats.Count + "] for message id = [" + messageId + + "]"); + } + } + + #endregion + + protected virtual void SendMessageToQueue(Message message) + { + MessageQueue mq = MessageQueueFactory.CreateMessageQueue(MessageQueueObjectName); + try + { + #region Logging + + if (LOG.IsInfoEnabled) + { + LOG.Info("Sending message with id = [" + message.Id + "] to queue [" + mq.QueueName + "]."); + } + + #endregion + + mq.Send(message, MessageQueueTransactionType.Automatic); + } + catch (Exception e) + { + #region Logging + + if (LOG.IsErrorEnabled) + { + LOG.Error("Could not send message with id = [" + message.Id + "] to queue [" + mq.QueueName + "].", e); + LOG.Error("Message will not be processed. Message Body = " + message.Body); + } + + #endregion + } + } + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/SendToQueueExceptionHandler.cs b/src/Spring/Spring.Messaging/Messaging/Listener/SendToQueueExceptionHandler.cs new file mode 100644 index 00000000..113da746 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/SendToQueueExceptionHandler.cs @@ -0,0 +1,182 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + + +using System; +using System.Messaging; +using Common.Logging; + +namespace Spring.Messaging.Listener +{ + public class SendToQueueExceptionHandler : AbstractSendToQueueExceptionHandler, IMessageTransactionExceptionHandler + { + #region Logging Definition + + private static readonly ILog LOG = LogManager.GetLogger(typeof (SendToQueueExceptionHandler)); + + #endregion + + #region Fields + + private string[] messageAlreadyProcessedExceptionNames; + + #endregion + + #region Properties + + /// + /// Gets or sets the exception anmes that indicate the message has already + /// been processed. If the exception thrown matches one of these names then + /// the returned TransactionAction is Commit to remove it from the queue. + /// + /// The name test is thrownException.GetType().Name.IndexOf(exceptionName) >= 0 + /// The message already processed exception types. + public string[] MessageAlreadyProcessedExceptionNames + { + set { messageAlreadyProcessedExceptionNames = value; } + get { return messageAlreadyProcessedExceptionNames; } + } + + #endregion + + #region IMessageTransactionExceptionHandler Members + + public TransactionAction OnException(Exception exception, Message message, + MessageQueueTransaction messageQueueTransaction) + { + if (IsMessageAlreadyProcessedException(exception)) + { + return TransactionAction.Commit; + } + + string messageId = message.Id; + lock (messageMapMonitor) + { + MessageStats messageStats = null; + if (messageMap.Contains(messageId)) + { + messageStats = (MessageStats) messageMap[messageId]; + } + else + { + messageStats = new MessageStats(); + messageMap[messageId] = messageStats; + } + messageStats.Count++; + LOG.Warn("Message Error Count = [" + messageStats.Count + "] for message id = [" + messageId + "]"); + + if (messageStats.Count > MaxRetry) + { + LOG.Info("Maximum number of redelivery attempts exceeded for message id = [" + messageId + "]"); + messageMap.Remove(messageId); + return SendMessageToQueue(message, messageQueueTransaction); + } + else + { + LOG.Warn("Rolling back delivery of message id [" + messageId + "]"); + return TransactionAction.Rollback; + } + } + } + + #endregion + + #region Protected Methods + + protected virtual bool IsMessageAlreadyProcessedException(Exception exception) + { + if (MessageAlreadyProcessedExceptionNames != null) + { + foreach (string exceptionName in MessageAlreadyProcessedExceptionNames) + { + if (exception.GetType().Name.IndexOf(exceptionName) >= 0) + { + return true; + } + } + } + return false; + } + + protected virtual TransactionAction SendMessageToQueue(Message message, + MessageQueueTransaction messageQueueTransaction) + { + MessageQueue mq = MessageQueueFactory.CreateMessageQueue(MessageQueueObjectName); + try + { + #region Logging + + if (LOG.IsInfoEnabled) + { + LOG.Info("Sending message with id = [" + message.Id + "] to queue [" + mq.QueueName + "]."); + } + + #endregion + + ProcessExceptionalMessage(message); + mq.Send(message, messageQueueTransaction); + } + catch (Exception e) + { + #region Logging + + if (LOG.IsErrorEnabled) + { + LOG.Error("Could not send message with id = [" + message.Id + "] to queue [" + mq.QueueName + "].",e); + LOG.Error("Message will not be processed. Message Body = " + message.Body); + } + + #endregion + } + return TransactionAction.Commit; + } + + /// + /// Template method called before the message that caused the exception is + /// send to another queue. The default behavior is to set the CorrelationId + /// to the current message's Id value for tracking purposes. Subclasses + /// can use other means, perhaps using the AppSpecific field or modifying the + /// body of the message to a known shared format that keeps track of + /// the full 'lifecycle' of the message as it goes from queue-to-queue. + /// + /// The message. + protected virtual void ProcessExceptionalMessage(Message message) + { + if (message.CorrelationId == null) + { + message.CorrelationId = message.Id; + } + } + + #endregion + } + + + internal class MessageStats + { + private int count; + + public int Count + { + get { return count; } + set { count = value; } + } + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/SimpleMessageListenerContainer.cs b/src/Spring/Spring.Messaging/Messaging/Listener/SimpleMessageListenerContainer.cs new file mode 100644 index 00000000..73b00e4e --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/SimpleMessageListenerContainer.cs @@ -0,0 +1,229 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + + +using System; +using System.Messaging; +using System.Threading; +using Common.Logging; + +namespace Spring.Messaging.Listener +{ + public class SimpleMessageListenerContainer : AbstractMessageListenerContainer + { + #region Logging Definition + + private static readonly ILog LOG = LogManager.GetLogger(typeof (SimpleMessageListenerContainer)); + + private Thread dispatcherThread; + + protected ManualResetEvent stopEvent = new ManualResetEvent(false); + + private IExceptionHandler exceptionHandler; + + private MessageQueue messageQueue; + + public MessageQueue MessageQueue + { + get { return messageQueue; } + set { messageQueue = value; } + } + + public IExceptionHandler ExceptionHandler + { + get { return exceptionHandler; } + set { exceptionHandler = value; } + } + + #endregion + + protected override void DoInitialize() + { + messageQueue = ApplicationContext.GetObject(MessageQueueObjectName, typeof (MessageQueue)) as MessageQueue; + } + + /// + /// Unsubscribe for messaging events and closethe queue + /// + protected override void DoShutdown() + { + CloseQueueHandle(MessageQueue); + if (dispatcherThread != null) + { + LOG.Debug("Waiting to join dispatcher thread."); + dispatcherThread.Join(); + dispatcherThread = null; + LOG.Debug("Dispatcher thread terminated."); + } + } + + /// + /// Re-initializes this container's consumers, if not initialized already. + /// + protected override void DoStart() + { + base.DoStart(); + stopEvent = new ManualResetEvent(false); + dispatcherThread = new Thread(new ThreadStart(StartListening)); + dispatcherThread.Start(); + } + + /// + /// Stops the container from listening to message events. + /// + public override void DoStop() + { + base.DoStop(); + CloseQueueHandle(MessageQueue); + stopEvent.Set(); + if (dispatcherThread != null) + { + LOG.Debug("Waiting to join dispatcher thread."); + dispatcherThread.Join(); + dispatcherThread = null; + LOG.Debug("Dispatcher thread terminated."); + } + } + + /// + /// Starts listening off the queue. + /// + protected virtual void StartListening() + { + if (Running) + { + try + { + IAsyncResult asynchResult = MessageQueue.BeginReceive(); + LOG.Debug("WaitAny"); + int firedWaitHandle = WaitHandle.WaitAny(new WaitHandle[] {asynchResult.AsyncWaitHandle, stopEvent}); + if (firedWaitHandle == 0) + { + ReceiveCompleted(asynchResult); + } + else + { + //Do the endreceive? + return; + } + } + catch (Exception ex) + { + LOG.Error( + "Exception executing DefaultMessageQueue.BeginReceive. Reinvoking after recovery interval [" + + RecoveryTimeSpan + "]", ex); + Thread.Sleep(RecoveryTimeSpan); + StartListening(); + } + } + } + + protected virtual void ReceiveCompleted(IAsyncResult asyncResult) + { + Message message; + + #region Receive Message + + try + { + LOG.Debug("ReceiveCompleted called."); + // Get reference to the queue. + + // End the asynchronous receive operation. + message = MessageQueue.EndReceive(asyncResult); + } + catch (MessageQueueException ex) + { + if (ex.MessageQueueErrorCode != MessageQueueErrorCode.IOTimeout) + { + if (LOG.IsTraceEnabled) + { + LOG.Trace("IOTimeout: Message to receive was already processed by another thread."); + } + } + else + { + // A real issue in receiving the message + LOG.Error("Error receiving message from DefaultMessageQueue = [" + MessageQueue.QueueName + "]"); + Thread.Sleep(RecoveryTimeSpan); + //InvokeReceiveExceptionHandler(ex);? + } + MessageQueue.Close(); + StartListening(); + return; + } + + #endregion + + if (message == null) + { + LOG.Error("Message recieved is null"); + StartListening(); + return; + } + + try + { + bool queued = ThreadPool.QueueUserWorkItem(new WaitCallback(ExecuteListener), message); + + #region Could not enqueue + + if (!queued) + { + LOG.Warn("Could not queue work item into thread pool. Retrying."); + Thread.Sleep(RecoveryTimeSpan); + } + + #endregion + } + catch (Exception e) + { + LOG.Error("Error enqueue message in thread pool DefaultMessageQueue = [" + MessageQueue.QueueName + "]", e); + Thread.Sleep(RecoveryTimeSpan); + } + finally + { + StartListening(); + } + } + + protected virtual void ExecuteListener(object state) + { + Message message = state as Message; + try + { + DoExecuteListener(message); + } + catch (Exception e) + { + HandleListenerException(e, message); + } + } + + private void HandleListenerException(Exception e, Message message) + { + IExceptionHandler exceptionHandler = ExceptionHandler; + if (exceptionHandler != null) + { + exceptionHandler.OnException(e, message); + } + } + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/TransactionAction.cs b/src/Spring/Spring.Messaging/Messaging/Listener/TransactionAction.cs new file mode 100644 index 00000000..599cc90d --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/TransactionAction.cs @@ -0,0 +1,38 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + + +namespace Spring.Messaging.Listener +{ + /// + /// Action to perform on the MessageQueueTransaction when handling message listener exceptions. + /// + public enum TransactionAction + { + /// + /// Rollback the MessageQueueTransaction, returning the recieved message back onto the queue. + /// + Rollback, + /// + /// Commit the MessageQueueTransaction, removing the message from the queue. + /// + Commit + } ; +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Listener/TransactionalMessageListenerContainer.cs b/src/Spring/Spring.Messaging/Messaging/Listener/TransactionalMessageListenerContainer.cs new file mode 100644 index 00000000..d4b95327 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Listener/TransactionalMessageListenerContainer.cs @@ -0,0 +1,649 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Messaging; +using Common.Logging; +using Spring.Data.Core; +using Spring.Messaging.Core; +using Spring.Messaging.Support; +using Spring.Transaction; +using Spring.Transaction.Support; + +namespace Spring.Messaging.Listener +{ + /// + /// A MessageListenerContainer that uses local (non-DTC) based transactions. Exceptions are + /// handled by instances of . + /// + /// + /// + /// This container distinguishes between two types of + /// implementations. + /// + /// If you specify a then + /// a MSMQ will be started + /// before receiving the message and used as part of the container's recieve operation. The + /// binds the + /// to thread local storage and as such will implicitly be used by + /// send and receive operations to a transactional queue. + /// + /// + /// Service layer operations that are called inside the message listener will typically + /// be transactional based on using standard Spring declarative transaction management + /// functionality. In case of exceptions in the service layer, the database operation + /// will have been rolled back and the + /// that is later invoked should decide to either commit the surrounding local + /// MSMQ based transaction (removing the message from the queue) or to rollback + /// (placing the message back on the queue for redelivery). + /// + /// + /// The use of a transactional service layer in combination with + /// a container managed is a powerful combination + /// that can be used to achieve "exactly one" transaction message processing with + /// database operations that are commonly associated with using transactional messaging and + /// distributed transactions (i.e. both the messaging and database operation commit or rollback + /// together). + /// + /// + /// The additional programming logic needed to achieve this is to keep track of the Message.Id + /// that has been processed successfully within the transactional service layer. + /// This is needed as there may be a system failure (e.g. power goes off) + /// between the 'inner' database commit and the 'outer' messaging commit, resulting + /// in message redelivery. The transactional service layer needs logic to detect if incoming + /// message was processed successfully. It can do this by checking the database for an + /// indication of successfull processing, perhaps by recording the Message.Id itself in a + /// status table. If the transactional service layer determines that the message has + /// already been processed, it can throw a specific exception for thise case. The + /// container's exception handler will recognize this exception type and vote to commit + /// (remove from the queue) the 'outer' messaging transaction. + /// Spring provides an exception handler with this functionality, + /// see for more information. + /// + /// If you specify an implementation of + /// (e.g. or HibernateTransactionManager) then + /// an local database transaction will be started before receiving the message. By default, + /// the container will also start a local + /// after the local database transaction has started, but before the receiving the message. + /// The will be used to receive the message. + /// If you do not want his behavior set + /// to false. Also by default, the + /// will be bound to thread local storage such that any + /// send or recieve operations will participate transparently in the same + /// . If you do not want this behavior + /// set the property to false. + /// + /// In case of exceptions during processing + /// when using an implementation of + /// (e.g. and starting a container managed + /// ) the container's + /// will determine if the + /// should commit (removing it from the queue) + /// or rollback (placing it back on the queue for redelivery). The listener + /// exception will always + /// trigger a rollback in the 'outer' (e.g. + /// or HibernateTransactionManager) based transaction. + /// + /// + /// PoisonMessage handing, that is endless redelivery of a message due to exceptions + /// during processing, can be detected using implementatons of the + /// interface. A specific implementation + /// is provided that will move the poison message to another queue after a maximum number + /// of redelivery attempts. See for more information. + /// + /// + public class TransactionalMessageListenerContainer : AbstractTransactionalMessageListenerContainer + { + #region Logging Definition + + private static readonly ILog LOG = LogManager.GetLogger(typeof (TransactionalMessageListenerContainer)); + + #endregion + + #region Fields + + private bool useContainerManagedMessageQueueTransaction = false; + + private bool useMessageQueueTransactionManagerCalled = false; + + private bool exposeContainerManagedMessageQueueTransaction = true; + + private IMessageTransactionExceptionHandler messageTransactionExceptionHandler; + + #endregion + + #region Properties + + /// + /// Gets or sets a value indicating whether the MessageListenerContainer should be + /// responsible for creating a MessageQueueTransaction + /// when receiving a message. + /// + /// + /// + /// Creating MessageQueueTransactions is usually the responsibility of the + /// IPlatformTransactionManager, e.g. TxScopePlatformTransactionManager (when using DTC) + /// or MessageQueueTransactionManager (when using local messaging transactions). + /// + /// + /// For all other IPlatformTransactionManager implementations, including when none is + /// specified, the MessageListenerContainer will itself create a MessageQueueTransaction + /// (assuming the container is consuming from a transactional queue). + /// + /// + /// Set the ExposeContainerManagedMessageQueueTransaction property to true if you want + /// the MessageQueueTransaction to be exposed to Spring's MessageQueueTemplate class + /// + /// + /// + /// true to use a container managed MessageQueueTransaction; otherwise, false. + /// + public bool UseContainerManagedMessageQueueTransaction + { + get { return useContainerManagedMessageQueueTransaction; } + set + { + useContainerManagedMessageQueueTransaction = value; + useMessageQueueTransactionManagerCalled = true; + } + } + + /// + /// Gets or sets a value indicating whether expose the + /// container managed to thread local storage + /// where it will be automatically used by send + /// and receive operations. + /// + /// + /// Using an will always exposes a + /// to thread local storage. This property + /// only has effect when using a non-DTC based + /// + /// + /// true if [expose container managed message queue transaction]; otherwise, false. + /// + public bool ExposeContainerManagedMessageQueueTransaction + { + get { return exposeContainerManagedMessageQueueTransaction; } + set { exposeContainerManagedMessageQueueTransaction = value; } + } + + /// + /// Gets or sets the message transaction exception handler. + /// + /// The message transaction exception handler. + public IMessageTransactionExceptionHandler MessageTransactionExceptionHandler + { + get { return messageTransactionExceptionHandler; } + set { messageTransactionExceptionHandler = value; } + } + + #endregion + + #region Public Methods + + /// + /// Determine if the container should create its own + /// MessageQueueTransaction when a IResourceTransactionManager is specified. + /// Set the transaction name to the name of the spring object. + /// Call base class Initialize() funtionality + /// + public override void Initialize() + { + //using non-DTC based transaction manager? + bool isRtm = PlatformTransactionManager is IResourceTransactionManager; + //using MessageQueueTransactionManager? + bool isQtm = PlatformTransactionManager is MessageQueueTransactionManager; + + if (!isRtm && !isQtm) + { + throw new ArgumentException("Can not use the provied IPlatformTransactionManager of type " + + PlatformTransactionManager.GetType() + + ". It must implement IResourceTransactionManager or be a MessageQueueTransactionManager."); + } + + //Set useMessageQueueTransactionManager = true when using + // 1. non-DTC based transaction manager + // 2. not the MessageQueueTransactionManager. + if (!useMessageQueueTransactionManagerCalled && isRtm && !isQtm) + { + useContainerManagedMessageQueueTransaction = true; + } + + // Use object name as default transaction name. + if (TransactionDefinition.Name == null) + { + TransactionDefinition.Name = ObjectName; + } + + // Proceed with superclass initialization. + base.Initialize(); + } + + #endregion + + #region Protected Methods + + protected override bool DoReceiveAndExecuteUsingPlatformTransactionManager(MessageQueue mq, + ITransactionStatus status) + { + if (PlatformTransactionManager is MessageQueueTransactionManager) + { + return DoRecieveAndExecuteUsingMessageQueueTransactionManager(mq, status); + } + else if (PlatformTransactionManager is IResourceTransactionManager) + { + if (UseContainerManagedMessageQueueTransaction) + { + return DoRecieveAndExecuteUsingResourceTransactionManagerWithTxQueue(mq, status); + } + else + { + //recieve non-transactionally from transactional queue but + //use ResourceBasedTransactionManagement. + DoRecieveAndExecuteUsingResourceTransactionManager(); + } + } + return false; + } + + private void DoRecieveAndExecuteUsingResourceTransactionManager() + { + //This is a bit of an odd case since really one is better off using + //NonTransactionalMessageListenerContainer and having the database + //transaction done in the service tier. + + throw new NotImplementedException("Try using NonTransactionalMessageListenerContainer instead."); + } + + protected virtual bool DoRecieveAndExecuteUsingMessageQueueTransactionManager(MessageQueue mq, + ITransactionStatus status) + { + #region Logging + + if (LOG.IsDebugEnabled) + { + LOG.Debug("Executing DoRecieveAndExecuteUsingMessageQueueTransactionManager"); + } + + #endregion Logging + + Message message; + + #region Receive message + + try + { + //TODO check that GetMessageQueueTransction doesn't return null. + message = mq.Receive(TimeSpan.Zero, QueueUtils.GetMessageQueueTransaction(null)); + } + catch (MessageQueueException ex) + { + if (ex.MessageQueueErrorCode != MessageQueueErrorCode.IOTimeout) + { + //expected to occur occasionally + if (LOG.IsTraceEnabled) + { + LOG.Trace("IOTimeout: Message to receive was already processed by another thread."); + } + status.RollbackOnly = true; + return false; // no more peeking unless this is the last listener thread + } + else + { + // A real issue in receiving the message + + #region Logging + + if (LOG.IsErrorEnabled) + { + LOG.Error("Error receiving message from DefaultMessageQueue [" + mq.QueueName + + "], closing queue and clearing connection cache."); + } + + #endregion + + lock (messageQueueMonitor) + { + mq.Close(); + MessageQueue.ClearConnectionCache(); + } + throw; // will cause rollback in MessageQueueTransactionManager and log exception + } + } + + #endregion + + if (message == null) + { + #region Logging + + if (LOG.IsTraceEnabled) + { + LOG.Trace("Message recieved is null from Queue = [" + mq.QueueName + "]"); + } + + #endregion + + status.RollbackOnly = true; + return false; // no more peeking unless this is the last listener thread + } + + try + { + #region Logging + + if (LOG.IsDebugEnabled) + { + LOG.Debug("Received message [" + message.Id + "] on queue [" + mq.QueueName + "]"); + } + + #endregion + + MessageReceived(message); + DoExecuteListener(message); + + #region Logging + + if (LOG.IsTraceEnabled) + { + LOG.Trace("MessageListener executed"); + } + + #endregion + } + catch (Exception ex) + { + //Exception may indicate rollback of database transaction in service layer. + //Let the handler determine if the message should be removed from the queue. + TransactionAction action = + HandleTransactionalListenerException(ex, message, QueueUtils.GetMessageQueueTransaction(null)); + if (action == TransactionAction.Rollback) + { + #region Logging + + if (LOG.IsDebugEnabled) + { + LOG.Debug( + "Exception handler's TransactionAction has rolled back MessageQueueTransaction for queue [" + + mq.QueueName + "]"); + } + + #endregion + + status.RollbackOnly = true; + return false; // no more peeking unless this is the last listener thread + } + else + { + LOG.Info("Committing MessageQueueTransaction due to explicit commit request by exception handler."); + } + } + finally + { + message.Dispose(); + } + return true; + } + + protected virtual bool DoRecieveAndExecuteUsingResourceTransactionManagerWithTxQueue(MessageQueue mq, + ITransactionStatus status) + { + #region Logging + + if (LOG.IsDebugEnabled) + { + LOG.Debug("Executing DoRecieveAndExecuteUsingResourceTransactionManagerWithTxQueue"); + } + + #endregion Logging + + using (MessageQueueTransaction messageQueueTransaction = new MessageQueueTransaction()) + { + messageQueueTransaction.Begin(); + + #region Logging + + if (LOG.IsTraceEnabled) + { + LOG.Trace("Started MessageQueueTransaction for queue = [" + mq.QueueName + "]"); + } + + #endregion + + Message message; + + #region ReceiveMessage + + try + { + #region Logging + + if (LOG.IsTraceEnabled) + { + LOG.Trace("Receiving message with zero timeout for queue = [" + mq.QueueName + "]"); + } + + #endregion + + message = mq.Receive(TimeSpan.Zero, messageQueueTransaction); + } + catch (MessageQueueException ex) + { + if (ex.MessageQueueErrorCode == MessageQueueErrorCode.IOTimeout) + { + //expected to occur occasionally + + #region Logging + + if (LOG.IsTraceEnabled) + { + LOG.Trace( + "MessageQueueErrorCode.IOTimeout: No message available to receive. May have been processed by another thread."); + } + + #endregion + + status.RollbackOnly = true; + return false; // no more peeking unless this is the last listener thread + } + else + { + // A real issue in receiving the message + + #region Logging + + if (LOG.IsErrorEnabled) + { + LOG.Error("Error receiving message from DefaultMessageQueue [" + mq.QueueName + + "], closing queue and clearing connection cache."); + } + + #endregion + + lock (messageQueueMonitor) + { + mq.Close(); + MessageQueue.ClearConnectionCache(); + } + throw; // will cause rollback in surrounding platform transaction manager and log exception + } + } + + #endregion + + if (message == null) + { + #region Logging + + if (LOG.IsTraceEnabled) + { + LOG.Trace("Message recieved is null from Queue = [" + mq.QueueName + "]"); + } + + #endregion + + status.RollbackOnly = true; + return false; // no more peeking unless this is the last listener thread + } + + try + { + #region Logging + + if (LOG.IsDebugEnabled) + { + LOG.Debug("Received message [" + message.Id + "] on queue [" + mq.QueueName + "]"); + } + + #endregion + + MessageReceived(message); + + if (ExposeContainerManagedMessageQueueTransaction) + { + TransactionSynchronizationManager.BindResource( + MessageQueueTransactionManager.CURRENT_TRANSACTION_SLOTNAME, + new LocallyExposedMessageQueueResourceHolder(messageQueueTransaction)); + } + + DoExecuteListener(message); + + #region Logging + + if (LOG.IsTraceEnabled) + { + LOG.Trace("MessageListener executed"); + } + + #endregion + + messageQueueTransaction.Commit(); + + #region Logging + + if (LOG.IsTraceEnabled) + { + LOG.Trace("Committed MessageQueueTransaction for queue [" + mq.QueueName + "]"); + } + + #endregion + } + catch (Exception ex) + { + TransactionAction action = + HandleTransactionalListenerException(ex, message, messageQueueTransaction); + if (action == TransactionAction.Rollback) + { + messageQueueTransaction.Abort(); + + #region Logging + + if (LOG.IsDebugEnabled) + { + LOG.Debug( + "Exception handler's TransactionAction has rolled back MessageQueueTransaction for queue [" + + mq.QueueName + "]"); + } + + #endregion + } + else + { + // Will remove from the message queue + messageQueueTransaction.Commit(); + + #region Logging + + if (LOG.IsDebugEnabled) + { + LOG.Debug( + "Exception handler's TransactionAction has committed MessageQueueTransaction for queue [" + + mq.QueueName + "]"); + } + + #endregion + } + //Outer db-tx will rollback + throw; + } + finally + { + if (ExposeContainerManagedMessageQueueTransaction) + { + TransactionSynchronizationManager.UnbindResource( + MessageQueueTransactionManager.CURRENT_TRANSACTION_SLOTNAME); + } + message.Dispose(); + } + return true; + } + } + + + protected virtual TransactionAction HandleTransactionalListenerException(Exception e, Message message, + MessageQueueTransaction + messageQueueTransaction) + { + try + { + TransactionAction transactionAction = + InvokeTransactionalExceptionListener(e, message, messageQueueTransaction); + if (Active) + { + // Regular case: failed while active. + // Log at error level. + LOG.Error("Execution of message listener failed", e); + } + else + { + // Rare case: listener thread failed after container shutdown. + // Log at debug level, to avoid spamming the shutdown log. + LOG.Debug("Listener exception after container shutdown", e); + } + return transactionAction; + } + catch (Exception ex) + { + LOG.Error("Exception invoking MessageTransactionExceptionHandler. Rolling back transaction.", ex); + return TransactionAction.Rollback; + } + } + + + protected virtual TransactionAction InvokeTransactionalExceptionListener(Exception e, Message message, + MessageQueueTransaction + messageQueueTransaction) + { + IMessageTransactionExceptionHandler exMessageTransaction = MessageTransactionExceptionHandler; + if (exMessageTransaction != null) + { + return exMessageTransaction.OnException(e, message, messageQueueTransaction); + } + else + { + LOG.Warn("No MessageTransactionExceptionHandler defined. Defaulting to TransactionAction.Rollback."); + return TransactionAction.Rollback; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/MessagingException.cs b/src/Spring/Spring.Messaging/Messaging/MessagingException.cs new file mode 100644 index 00000000..4919c167 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/MessagingException.cs @@ -0,0 +1,59 @@ +using System; +using System.Runtime.Serialization; + +namespace Spring.Messaging +{ + public class MessagingException : ApplicationException + { + #region Constructor (s) / Destructor + + /// Creates a new instance of the MessagingException class. + public MessagingException() + { + } + + /// + /// Creates a new instance of the MessagingException class. with the specified message. + /// + /// + /// A message about the exception. + /// + public MessagingException(string message) : base(message) + { + } + + /// + /// Creates a new instance of the MessagingException class with the specified message + /// and root cause. + /// + /// + /// A message about the exception. + /// + /// + /// The root exception that is being wrapped. + /// + public MessagingException(string message, Exception rootCause) + : base(message, rootCause) + { + } + + /// + /// Creates a new instance of the MessagingException class. + /// + /// + /// The + /// that holds the serialized object data about the exception being thrown. + /// + /// + /// The + /// that contains contextual information about the source or destination. + /// + protected MessagingException( + SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Support/Converters/ActiveXMessageConverter.cs b/src/Spring/Spring.Messaging/Messaging/Support/Converters/ActiveXMessageConverter.cs new file mode 100644 index 00000000..a95057e7 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Support/Converters/ActiveXMessageConverter.cs @@ -0,0 +1,70 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + + +using System; +using System.Messaging; + +namespace Spring.Messaging.Support.Converters +{ + public class ActiveXMessageConverter : IMessageConverter + { + private ActiveXMessageFormatter messageFormatter; + + + public ActiveXMessageConverter() + { + messageFormatter = new ActiveXMessageFormatter(); + } + + public ActiveXMessageConverter(ActiveXMessageFormatter messageFormatter) + { + this.messageFormatter = messageFormatter; + } + + #region IMessageConverter Members + + public Message ToMessage(object obj) + { + Message m = new Message(); + m.Body = obj; + m.Formatter = messageFormatter; + return m; + } + + public object FromMessage(Message message) + { + message.Formatter = messageFormatter; + return message.Body; + } + + #endregion + + #region ICloneable Members + + public object Clone() + { + ActiveXMessageConverter mc = new ActiveXMessageConverter(messageFormatter.Clone() as ActiveXMessageFormatter); + return mc; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Support/Converters/BinaryMessageConverter.cs b/src/Spring/Spring.Messaging/Messaging/Support/Converters/BinaryMessageConverter.cs new file mode 100644 index 00000000..df1608bf --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Support/Converters/BinaryMessageConverter.cs @@ -0,0 +1,85 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + + +using System; +using System.Messaging; +using System.Runtime.Serialization.Formatters; + +namespace Spring.Messaging.Support.Converters +{ + public class BinaryMessageConverter : IMessageConverter + { + private BinaryMessageFormatter binaryMessageFormatter; + + private FormatterTypeStyle typeFormat; + private FormatterAssemblyStyle topObjectFormat; + public BinaryMessageConverter() + { + binaryMessageFormatter = new BinaryMessageFormatter(); + } + + + public BinaryMessageConverter(BinaryMessageFormatter binaryMessageFormatter) + { + this.binaryMessageFormatter = binaryMessageFormatter; + } + + public FormatterTypeStyle TypeFormat + { + get { return typeFormat; } + set { typeFormat = value; } + } + + public FormatterAssemblyStyle TopObjectFormat + { + get { return topObjectFormat; } + set { topObjectFormat = value; } + } + + #region IMessageConverter Members + + public Message ToMessage(object obj) + { + Message m = new Message(); + m.Body = obj; + m.Formatter = binaryMessageFormatter; + return m; + } + + public object FromMessage(Message message) + { + message.Formatter = binaryMessageFormatter; + return message.Body; + } + + #endregion + + #region ICloneable Members + + public object Clone() + { + BinaryMessageConverter mc = new BinaryMessageConverter(binaryMessageFormatter.Clone() as BinaryMessageFormatter); + return mc; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Support/Converters/IMessageConverter.cs b/src/Spring/Spring.Messaging/Messaging/Support/Converters/IMessageConverter.cs new file mode 100644 index 00000000..ceecaf95 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Support/Converters/IMessageConverter.cs @@ -0,0 +1,42 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Messaging; + +namespace Spring.Messaging.Support.Converters +{ + public interface IMessageConverter : ICloneable + { + /// + /// Convert the given object to a Message. + /// + /// The object to send. + /// Message to send + Message ToMessage(object obj); + + /// + /// Convert the given message to a object. + /// + /// The message. + /// the object + object FromMessage(Message message); + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Support/Converters/XmlDocumentConverter.cs b/src/Spring/Spring.Messaging/Messaging/Support/Converters/XmlDocumentConverter.cs new file mode 100644 index 00000000..237648b5 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Support/Converters/XmlDocumentConverter.cs @@ -0,0 +1,46 @@ + + +using System; +using System.Messaging; +using System.Xml; + +namespace Spring.Messaging.Support.Converters +{ + public class XmlDocumentConverter : IMessageConverter + { + #region IMessageConverter Members + + public Message ToMessage(object obj) + { + XmlDocument doc = obj as XmlDocument; + if (doc != null) + { + Message m = new Message(); + doc.Save(m.BodyStream); + return m; + } + else + { + throw new MessagingException("Expected object to be of type System.Xml.XmlDocument"); + } + } + + public object FromMessage(Message message) + { + XmlDocument doc = new XmlDocument(); + doc.Load(message.BodyStream); + return doc; + } + + #endregion + + #region ICloneable Members + + public object Clone() + { + throw new NotImplementedException(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Support/Converters/XmlMessageConverter.cs b/src/Spring/Spring.Messaging/Messaging/Support/Converters/XmlMessageConverter.cs new file mode 100644 index 00000000..37c5befd --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Support/Converters/XmlMessageConverter.cs @@ -0,0 +1,90 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + + +using System; +using System.Messaging; +using Spring.Util; + +namespace Spring.Messaging.Support.Converters +{ + public class XmlMessageConverter : IMessageConverter + { + private XmlMessageFormatter messageFormatter; + + public XmlMessageConverter() + { + messageFormatter = new XmlMessageFormatter(); + } + + public XmlMessageConverter(XmlMessageFormatter messageFormatter) + { + this.messageFormatter = messageFormatter; + } + + public Type[] TargetTypes + { + set + { + AssertUtils.ArgumentNotNull(value, "TargetTypes"); + messageFormatter.TargetTypes = value; + } + get { return messageFormatter.TargetTypes; } + } + + public string[] TargetTypeNames + { + set + { + AssertUtils.ArgumentNotNull(value, "TargetTypeNames"); + messageFormatter.TargetTypeNames = value; + } + get { return messageFormatter.TargetTypeNames; } + } + + #region IMessageConverter Members + + public Message ToMessage(object obj) + { + Message m = new Message(); + m.Body = obj; + m.Formatter = messageFormatter; + return m; + } + + public object FromMessage(Message message) + { + message.Formatter = messageFormatter; + return message.Body; + } + + #endregion + + #region ICloneable Members + + public object Clone() + { + XmlMessageConverter mc = new XmlMessageConverter(messageFormatter.Clone() as XmlMessageFormatter); + return mc; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Support/MessageQueueFactoryObject.cs b/src/Spring/Spring.Messaging/Messaging/Support/MessageQueueFactoryObject.cs new file mode 100644 index 00000000..ab0f3ae7 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Support/MessageQueueFactoryObject.cs @@ -0,0 +1,188 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Messaging; +using Spring.Objects.Factory.Config; + +namespace Spring.Messaging.Support +{ + /// + /// Factory for creating MessageQueues + /// + /// Mark Pollack + public class MessageQueueFactoryObject : IConfigurableFactoryObject + { + // fields used in constructor + private string path = string.Empty; + private bool modeDenySharedReceive; + private bool enableCache; + private QueueAccessMode accessMode = QueueAccessMode.SendAndReceive; + + // To avoid stale handles when MSMQ restarts and when stopping async recieve/peek operations + private bool enableConnectionCache = false; + + private IObjectDefinition productTemplate; + + private bool messageReadPropertyFilterSetAll = true; + + private bool messageReadPropertyFilterSetDefaults = false; + + //myQueue.MessageReadPropertyFilter.SetAll(); + + /// + /// Gets or sets the path used to creat DefaultMessageQueue instance. + /// + /// The location of the queue referenced by the DefaultMessageQueue. + public string Path + { + get { return path; } + set { path = value; } + } + + + /// + /// Gets or sets a value indicating whether to create the DefaultMessageQueue instance with + /// exclusive read access to the first application that accesses the queue + /// + /// + /// true to grant exclusive read access to the first application that accesses the queue; otherwise, false. + /// + public bool DenySharedReceive + { + get { return modeDenySharedReceive; } + set { modeDenySharedReceive = value; } + } + + + /// + /// Gets or sets the queue access mode. + /// + /// The queue access mode. + /// + public QueueAccessMode AccessMode + { + get { return accessMode; } + set { accessMode = value; } + } + + + /// + /// Gets or sets a value indicating whether [enable cache]. + /// + /// true to create and use a connection cache; otherwise false. + public bool EnableCache + { + get { return enableCache; } + set { enableCache = value; } + } + + /// + /// Sets a value indicating whether to enable connection cache. The default is false, which + /// is different than the default value when creating a DefaultMessageQueue object. + /// + /// + /// true if enable connection cache; otherwise, false. + /// + public bool EnableConnectionCache + { + set { enableConnectionCache = value; } + } + + + /// + /// Sets a value indicating whether to retrieve all message properties when receiving a message. + /// + /// + /// true if should etrieve all message properties when receiving a message; otherwise, false. + /// + public bool MessageReadPropertyFilterSetAll + { + set { messageReadPropertyFilterSetAll = value; } + } + + + /// + /// Sets a value indicating whether to set the filter values of common Message Queuing properties + /// to true and the integer-valued properties to their default values.. + /// + /// + /// true if should set the filter values of common Message Queuing properties; otherwise, false. + /// + public bool MessageReadPropertyFilterSetDefaults + { + set { messageReadPropertyFilterSetDefaults = value; } + } + + #region IFactoryObject Members + + public object GetObject() + { + MessageQueue.EnableConnectionCache = enableConnectionCache; + MessageQueue mq = new MessageQueue(Path, DenySharedReceive, EnableCache, AccessMode); + if (messageReadPropertyFilterSetDefaults) + { + mq.MessageReadPropertyFilter.SetDefaults(); + } + if (messageReadPropertyFilterSetAll) + { + mq.MessageReadPropertyFilter.SetAll(); + } + return mq; + } + + /// + /// Return the of object that this + /// creates, or + /// if not known in advance. + /// + /// The type DefaultMessageQueue + public Type ObjectType + { + get { return typeof (MessageQueue); } + } + + /// + /// Is the object managed by this factory a singleton or a prototype? + /// + /// return false, a new object will be created for each request of the object + public bool IsSingleton + { + get { return false; } + } + + #region IConfigurableFactoryObject Members + + /// + /// Gets the template object definition that should be used + /// to configure the instance of the object managed by this factory. + /// + /// The object definition to configure the factory's product + public IObjectDefinition ProductTemplate + { + get { return productTemplate; } + set { productTemplate = value; } + } + + #endregion + + #endregion + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Messaging/Support/QueueUtils.cs b/src/Spring/Spring.Messaging/Messaging/Support/QueueUtils.cs new file mode 100644 index 00000000..60814c08 --- /dev/null +++ b/src/Spring/Spring.Messaging/Messaging/Support/QueueUtils.cs @@ -0,0 +1,151 @@ +#region License + +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Messaging; +using Spring.Context; +using Spring.Messaging.Core; +using Spring.Messaging.Support.Converters; +using Spring.Objects.Factory.Support; +using Spring.Transaction.Support; + +namespace Spring.Messaging.Support +{ + public class QueueUtils + { + + /// + /// Registers the default message converter with the application context. + /// + /// The application context. + /// The name of the message converter to use for lookups with + /// . + /// + public static string RegisterDefaultMessageConverter(IApplicationContext applicationContext) + { + //Create a default message converter to use. + RootObjectDefinition rod = new RootObjectDefinition(typeof(XmlMessageConverter)); + rod.PropertyValues.Add("TargetTypes", new Type[] { typeof(String) }); + rod.IsSingleton = false; + IConfigurableApplicationContext ctx = (IConfigurableApplicationContext)applicationContext; + DefaultListableObjectFactory of = (DefaultListableObjectFactory)ctx.ObjectFactory; + string messageConverterObjectName = "__XmlMessageConverter__"; + if (!applicationContext.ContainsObjectDefinition(messageConverterObjectName)) + { + of.RegisterObjectDefinition(messageConverterObjectName, rod); + } + return messageConverterObjectName; + + } + public static MessageQueueTransaction GetMessageQueueTransaction(IResourceFactory resourceFactory) + { + MessageQueueResourceHolder resourceHolder = + (MessageQueueResourceHolder) + TransactionSynchronizationManager.GetResource( + MessageQueueTransactionManager.CURRENT_TRANSACTION_SLOTNAME); + if (resourceHolder != null) + { + return resourceHolder.MessageQueueTransaction; + } + if (!TransactionSynchronizationManager.SynchronizationActive) + { + return null; + } + throw new NotImplementedException(); + /* + MessageQueueResourceHolder resourceHolderToUse = resourceHolder; + if (resourceHolderToUse == null) + { + resourceHolderToUse = new MessageQueueResourceHolder(new MessageQueueTransaction()); + } + if (resourceHolderToUse != resourceHolder) + { + TransactionSynchronizationManager.RegisterSynchronization( + new MessageQueueResourceSynchronization(resourceHolderToUse, resourceFactory.SynchedLocalTransactionAllowed)); + resourceHolderToUse.SynchronizedWithTransaction = true; + TransactionSynchronizationManager.BindResource(MessageQueueTransactionManager.CURRENT_TRANSACTION_SLOTNAME, resourceHolderToUse); + } + return resourceHolderToUse.MessageQueueTransaction;*/ + } + } + + internal class MessageQueueResourceSynchronization : ITransactionSynchronization + { + private object resourceKey; + + private MessageQueueResourceHolder resourceHolder; + + private bool holderActive = true; + + public MessageQueueResourceSynchronization(MessageQueueResourceHolder resourceHolder, object resourceKey) + { + this.resourceHolder = resourceHolder; + this.resourceKey = resourceKey; + } + + #region ITransactionSynchronization Members + + public void Suspend() + { + if (holderActive) + { + TransactionSynchronizationManager.UnbindResource(resourceKey); + } + } + + public void Resume() + { + if (holderActive) + { + TransactionSynchronizationManager.BindResource(resourceKey, resourceHolder); + } + } + + public void BeforeCommit(bool readOnly) + { + throw new NotImplementedException(); + } + + public void AfterCommit() + { + throw new NotImplementedException(); + } + + public void BeforeCompletion() + { + TransactionSynchronizationManager.UnbindResource(resourceKey); + holderActive = false; + //this.resourceHolder.closeAll(); + throw new NotImplementedException(); + } + + public void AfterCompletion(TransactionSynchronizationStatus status) + { + throw new NotImplementedException(); + } + + #endregion + } + + public interface IResourceFactory + { + bool SynchedLocalTransactionAllowed { get; } + } +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Spring.Messaging.2005.csproj b/src/Spring/Spring.Messaging/Spring.Messaging.2005.csproj new file mode 100644 index 00000000..e5dc190b --- /dev/null +++ b/src/Spring/Spring.Messaging/Spring.Messaging.2005.csproj @@ -0,0 +1,109 @@ + + + Debug + AnyCPU + 8.0.50727 + 2.0 + {0E23AE41-D8D8-41C2-84A2-D35564049F0D} + Library + Properties + Spring + Spring.Messaging + + + true + full + false + ..\..\..\build\VS.Net.2005\Spring.Messaging\Debug\ + TRACE;DEBUG;NET_2_0 + prompt + 4 + Spring.Messaging.xml + + + pdbonly + true + ..\..\..\build\VS.Net.2005\Spring.Messaging\Release\ + TRACE + prompt + 4 + + + + False + ..\..\..\lib\Net\2.0\antlr.runtime.dll + + + False + ..\..\..\lib\Net\2.0\Common.Logging.dll + + + + + + + + + + CommonAssemblyInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {710961A3-0DF4-49E4-A26E-F5B9C044AC84} + Spring.Core.2005 + + + {AE00E5AB-C39A-436F-86D2-33BFE33E2E40} + Spring.Data.2005 + + + + + + + + + + + \ No newline at end of file diff --git a/src/Spring/Spring.Messaging/Spring.Messaging.build b/src/Spring/Spring.Messaging/Spring.Messaging.build new file mode 100644 index 00000000..899e8ceb --- /dev/null +++ b/src/Spring/Spring.Messaging/Spring.Messaging.build @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Core/MessageQueueTemplateTests.cs b/test/Spring/Spring.Messaging.Tests/Messaging/Core/MessageQueueTemplateTests.cs new file mode 100644 index 00000000..07352779 --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Core/MessageQueueTemplateTests.cs @@ -0,0 +1,182 @@ +#region License + +/* + * Copyright © 2002-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +#region Imports + +using System; +using System.Transactions; +using NUnit.Framework; +using Spring.Data.Core; +using Spring.Messaging.Support.Converters; +using Spring.Testing.NUnit; +using Spring.Threading; +using Spring.Transaction; +using Spring.Transaction.Support; +using Spring.Util; + +#endregion + +namespace Spring.Messaging.Core +{ + /// + /// This class contains tests for + /// + /// Mark Pollack + /// $Id:$ + [TestFixture] + public class MessageQueueTemplateTests : AbstractDependencyInjectionSpringContextTests + { + [Test] + [ExpectedException(typeof (ArgumentException), ExpectedMessage = "DefaultMessageQueueObjectName is required.")] + public void NoMessageQueueNameSpecified() + { + MessageQueueTemplate mqt = new MessageQueueTemplate(); + mqt.AfterPropertiesSet(); + } + + [Test] + [ExpectedException(typeof (ArgumentException), + ExpectedMessage = "No object named noqueuename is defined in the Spring container")] + public void MessageQueueNameNotInContext() + { + MessageQueueTemplate q = new MessageQueueTemplate("noqueuename"); + q.ApplicationContext = applicationContext; + q.AfterPropertiesSet(); + } + + [Test] + public void MessageQueueCreatedinThreadLocalStorage() + { + MessageQueueTemplate q = applicationContext["queue"] as MessageQueueTemplate; + Assert.IsNotNull(q); + Assert.AreEqual(q.DefaultMessageQueue, q.MessageQueueFactory.CreateMessageQueue(q.DefaultMessageQueueObjectName)); + } + + [Test] + [ExpectedException(typeof (InvalidOperationException), + ExpectedMessage = "No MessageConverter registered. Check configuration of MessageQueueTemplate.")] + public void MessageConverterNotRegistered() + { + MessageQueueTemplate q = applicationContext["queue-noconverter"] as MessageQueueTemplate; + Assert.IsNotNull(q); + IMessageConverter c = q.MessageConverter; + } + + #region Integration Tests - to be moved to another test assembly + + [Test] + public void SendAndReceiveNonTransactional() + { + MessageQueueTemplate q = applicationContext["queue"] as MessageQueueTemplate; + Assert.IsNotNull(q); + ReceiveHelloWorld(q,1); + } + + private static void ReceiveHelloWorld(MessageQueueTemplate q, int index) + { + object o = q.ReceiveAndConvert(); + Assert.IsNotNull(o); + string data = o as string; + Assert.IsNotNull(data); + Assert.AreEqual("Hello World " + index, data); + } + + [Test] + public void SendNonTxMessageQueueUsingMessageTx() + { + MessageQueueTemplate q = applicationContext["queue"] as MessageQueueTemplate; + Assert.IsNotNull(q); + SendAndRecieve(q); + } + + [Test] + public void SendTxMessageQueueUsingMessageTx() + { + MessageQueueTemplate q = applicationContext["txqueue"] as MessageQueueTemplate; + Assert.IsNotNull(q); + SendAndRecieve(q); + } + + [Test] + public void SendTxMessageQueueUsingTxScope() + { + MessageQueueTemplate q = applicationContext["txqueue"] as MessageQueueTemplate; + Assert.IsNotNull(q); + SendUsingMessageTxScope(q); + Receive(q); + } + + private static void SendAndRecieve(MessageQueueTemplate q) + { + SendUsingMessageTx(q); + Receive(q); + } + + private static void Receive(MessageQueueTemplate q) + { + ReceiveHelloWorld(q,1); + ReceiveHelloWorld(q,2); + ReceiveHelloWorld(q,3); + } + + private static void SendUsingMessageTx(MessageQueueTemplate q) + { + IPlatformTransactionManager txManager = new MessageQueueTransactionManager(); + TransactionTemplate transactionTemplate = new TransactionTemplate(txManager); + transactionTemplate.Execute(delegate(ITransactionStatus status) + { + q.ConvertAndSend("Hello World 1"); + q.ConvertAndSend("Hello World 2"); + q.ConvertAndSend("Hello World 3"); + return null; + }); + } + + private static void SendUsingMessageTxScope(MessageQueueTemplate q) + { + IPlatformTransactionManager txManager = new TxScopeTransactionManager(); + TransactionTemplate transactionTemplate = new TransactionTemplate(txManager); + transactionTemplate.Execute(delegate(ITransactionStatus status) + { + q.ConvertAndSend("Hello World 1"); + q.ConvertAndSend("Hello World 2"); + q.ConvertAndSend("Hello World 3"); + return null; + }); + } + + #endregion + + [Test] + public void GetAllFromQueue() + { + MessageQueueTemplate q = applicationContext["queue"] as MessageQueueTemplate; + Assert.IsNotNull(q); + for (int i = 0; i < 5; i++) + { + Console.WriteLine(q.ReceiveAndConvert()); + } + } + protected override string[] ConfigLocations + { + get { return new string[] {"assembly://Spring.Messaging.Tests/Spring.Messaging.Core/MessageQueueTemplateTests.xml"}; } + } + } +} \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Core/MessageQueueTemplateTests.xml b/test/Spring/Spring.Messaging.Tests/Messaging/Core/MessageQueueTemplateTests.xml new file mode 100644 index 00000000..a69cc396 --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Core/MessageQueueTemplateTests.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Core/ThreadingTests.cs b/test/Spring/Spring.Messaging.Tests/Messaging/Core/ThreadingTests.cs new file mode 100644 index 00000000..039c4bfc --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Core/ThreadingTests.cs @@ -0,0 +1,56 @@ +#region License + +/* + * Copyright © 2002-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +#region Imports + +using System.Threading; +using NUnit.Framework; + +#endregion + +namespace Spring.Messaging.Core +{ + /// + /// This class contains tests for + /// + /// Mark Pollack + /// $Id:$ + [TestFixture] + public class ThreadingTests + { + private int activeListenerCount = 0; + [SetUp] + public void Setup() + { + } + + [Test] + public void Test() + { + Interlocked.Increment(ref activeListenerCount); + // just gets the current value... + int count = Interlocked.CompareExchange(ref activeListenerCount, -1, -1); + Assert.AreEqual(1, count); + + } + + + } +} \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Listener/DistributedTxMessageListenerContainerTests.cs b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/DistributedTxMessageListenerContainerTests.cs new file mode 100644 index 00000000..c4b8e5b8 --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/DistributedTxMessageListenerContainerTests.cs @@ -0,0 +1,116 @@ +#region License + +/* + * Copyright © 2002-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +#region Imports + +using System.Threading; +using NUnit.Framework; +using Spring.Messaging.Core; +using Spring.Testing.NUnit; + +#endregion + +namespace Spring.Messaging.Listener +{ + /// + /// This class contains tests for DistributedTxMessageListenerContainer + /// + /// Mark Pollack + [TestFixture] + public class DistributedTxMessageListenerContainerTests : AbstractDependencyInjectionSpringContextTests + { + private int waitInMillis = 20000; + private DistributedTxMessageListenerContainer distributedTxMessageListenerContainer; + private SimpleHandler listener; + + + public DistributedTxMessageListenerContainer DistributedTxMessageListenerContainer + { + set { distributedTxMessageListenerContainer = value; } + } + + + public SimpleHandler Listener + { + set { listener = value; } + } + + [Test] + public void SendAndAsyncReceiveWithExceptionHandling() + { + MessageQueueTemplate q = applicationContext["queueTemplate"] as MessageQueueTemplate; + Assert.IsNotNull(q); + + MessageQueueTemplate retryQ = applicationContext["retryQueueTemplate"] as MessageQueueTemplate; + Assert.IsNotNull(retryQ); + + q.ConvertAndSend("Goodbye World 1"); + + Assert.AreEqual(0, listener.MessageCount); + distributedTxMessageListenerContainer.Start(); + + Thread.Sleep(waitInMillis); + + + distributedTxMessageListenerContainer.Stop(); + distributedTxMessageListenerContainer.Shutdown(); + Thread.Sleep(2500); + + object msg = retryQ.ReceiveAndConvert(); + Assert.IsNotNull(msg); + string textMsg = msg as string; + Assert.IsNotNull(textMsg); + Assert.AreEqual("Goodbye World 1", textMsg); + } + + + [Test] + public void SendAndAsyncReceive() + { + + + MessageQueueTemplate q = applicationContext["queueTemplate"] as MessageQueueTemplate; + Assert.IsNotNull(q); + + q.ConvertAndSend("Hello World 1"); + q.ConvertAndSend("Hello World 2"); + q.ConvertAndSend("Hello World 3"); + q.ConvertAndSend("Hello World 4"); + q.ConvertAndSend("Hello World 5"); + + Assert.AreEqual(0, listener.MessageCount); + + distributedTxMessageListenerContainer.Start(); + + Thread.Sleep(waitInMillis); + Assert.AreEqual(5, listener.MessageCount); + + distributedTxMessageListenerContainer.Stop(); + distributedTxMessageListenerContainer.Shutdown(); + Thread.Sleep(2500); + + } + + protected override string[] ConfigLocations + { + get { return new string[] { "assembly://Spring.Messaging.Tests/Spring.Messaging.Listener/DistributedTxMessageListenerContainerTests.xml" }; } + } + } +} \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Listener/DistributedTxMessageListenerContainerTests.xml b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/DistributedTxMessageListenerContainerTests.xml new file mode 100644 index 00000000..21a9b6c5 --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/DistributedTxMessageListenerContainerTests.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Listener/LoggingExceptionHandler.cs b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/LoggingExceptionHandler.cs new file mode 100644 index 00000000..42f0bb15 --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/LoggingExceptionHandler.cs @@ -0,0 +1,68 @@ +using System; +using System.Messaging; +using System.Threading; +using Common.Logging; + +namespace Spring.Messaging.Listener +{ + /// + /// + /// + public class LoggingExceptionHandler : IExceptionHandler + { + private TimeSpan recoveryTimeSpan; + + #region Logging Definition + + private static readonly ILog LOG = LogManager.GetLogger(typeof (LoggingExceptionHandler)); + + #endregion + + /// + /// Initializes a new instance of the class with + /// a default recovery time span of 5 seconds. + /// + public LoggingExceptionHandler() + { + recoveryTimeSpan = new TimeSpan(0, 0, 0, 5); + } + + public LoggingExceptionHandler(TimeSpan recoveryTimeSpan) + { + this.recoveryTimeSpan = recoveryTimeSpan; + } + + public TimeSpan RecoveryTimeSpan + { + set { recoveryTimeSpan = value; } + } + + #region IExceptionListener Members + + public void OnException(Exception exception, Message message) + { + //TODO other exception handling + MessageQueueException e = exception as MessageQueueException; + if (e != null) + { + switch ((int) e.MessageQueueErrorCode) + { + case (int) MessageQueueErrorCode.IOTimeout: + case -1073741536: + Console.WriteLine("Msmq Error -1073741536 or IOTimeout : Sleeping, and then ReListening"); + Thread.Sleep(recoveryTimeSpan); + break; + default: + LOG.Error("Exception Receiving Message", e); + break; + } + } + else + { + LOG.Error("got exception", exception); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Listener/NonTransactionalMessageListenerContainerTests.cs b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/NonTransactionalMessageListenerContainerTests.cs new file mode 100644 index 00000000..a6b7c5ea --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/NonTransactionalMessageListenerContainerTests.cs @@ -0,0 +1,114 @@ +#region License + +/* + * Copyright © 2002-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +#region Imports + +using System.Threading; +using NUnit.Framework; +using Spring.Messaging.Core; +using Spring.Testing.NUnit; + +#endregion + +namespace Spring.Messaging.Listener +{ + /// + /// This class contains tests for + /// + /// Mark Pollack + /// $Id:$ + [TestFixture] + public class NonTransactionalMessageListenerContainerTests : AbstractDependencyInjectionSpringContextTests + { + + private int waitInMillis = 20000; + private NonTransactionalMessageListenerContainer container; + private SimpleHandler listener; + private SimpleExceptionHandler exceptionHandler; + + [Test] + public void Test() + { + } + + public SimpleExceptionHandler ExceptionHandler + { + set { exceptionHandler = value; } + } + + public NonTransactionalMessageListenerContainer Container + { + get { return container; } + set { container = value; } + } + + public SimpleHandler Listener + { + get { return listener; } + set { listener = value; } + } + + [Test] + public void SendAndAsyncReceiveWithExceptionHandling() + { + MessageQueueTemplate q = applicationContext["testQueueTemplate"] as MessageQueueTemplate; + Assert.IsNotNull(q); + q.ConvertAndSend("Goodbye World 1"); + Assert.AreEqual(0, listener.MessageCount); + container.Start(); + Thread.Sleep(waitInMillis); + Assert.AreEqual(0, listener.MessageCount); + Assert.AreEqual(1, exceptionHandler.MessageCount); + container.Stop(); + container.Shutdown(); + Thread.Sleep(2500); + } + + [Test] + public void SendAndAsyncReceive() + { + MessageQueueTemplate q = applicationContext["testQueueTemplate"] as MessageQueueTemplate; + Assert.IsNotNull(q); + + q.ConvertAndSend("Hello World 1"); + q.ConvertAndSend("Hello World 2"); + q.ConvertAndSend("Hello World 3"); + q.ConvertAndSend("Hello World 4"); + q.ConvertAndSend("Hello World 5"); + + Assert.AreEqual(0, listener.MessageCount); + + container.Start(); + + Thread.Sleep(waitInMillis); + Assert.AreEqual(5, listener.MessageCount); + Assert.AreEqual(0, exceptionHandler.MessageCount); + + container.Stop(); + container.Shutdown(); + Thread.Sleep(2500); + } + + protected override string[] ConfigLocations + { + get { return new string[] { "assembly://Spring.Messaging.Tests/Spring.Messaging.Listener/NonTransactionalMessageListenerContainerTests.xml" }; } + } + } +} \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Listener/NonTransactionalMessageListenerContainerTests.xml b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/NonTransactionalMessageListenerContainerTests.xml new file mode 100644 index 00000000..3f46da96 --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/NonTransactionalMessageListenerContainerTests.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleExceptionHandler.cs b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleExceptionHandler.cs new file mode 100644 index 00000000..9dc3cf41 --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleExceptionHandler.cs @@ -0,0 +1,36 @@ + + +using System; +using System.Messaging; +using Common.Logging; + +namespace Spring.Messaging.Listener +{ + public class SimpleExceptionHandler : IExceptionHandler + { + #region Logging + + private static readonly ILog LOG = LogManager.GetLogger(typeof(SimpleExceptionHandler)); + + #endregion + + private int messageCount; + + + public int MessageCount + { + get { return messageCount; } + set { messageCount = value; } + } + + #region IExceptionHandler Members + + public void OnException(Exception exception, Message message) + { + LOG.Error("Exception Handler processing message id = [" + message.Id + "]"); + messageCount++; + } + + #endregion + } +} \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleHandler.cs b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleHandler.cs new file mode 100644 index 00000000..daa0ea51 --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleHandler.cs @@ -0,0 +1,37 @@ + + +using System; +using Common.Logging; + +namespace Spring.Messaging.Listener +{ + public class SimpleHandler + { + #region Logging + + private static readonly ILog LOG = LogManager.GetLogger(typeof(SimpleHandler)); + + #endregion + + private int messageCount; + + + public int MessageCount + { + get { return messageCount; } + set { messageCount = value; } + } + + public string HandleMessage(string msgTxt) + { + LOG.Debug("Received text = [" + msgTxt + "]"); + if (msgTxt.Contains("Goodbye")) + { + throw new ArgumentException("Don't like saying goodbye!"); + } + messageCount++; + LOG.Debug("Message listener count = " + messageCount); + return msgTxt + " - processed!"; + } + } +} \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleMessageListener.cs b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleMessageListener.cs new file mode 100644 index 00000000..9faa6226 --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleMessageListener.cs @@ -0,0 +1,40 @@ + + +using System.Messaging; +using Common.Logging; + +namespace Spring.Messaging.Listener +{ + public class SimpleMessageListener : IMessageListener + { + #region Logging Definition + + private static readonly ILog LOG = LogManager.GetLogger(typeof(SimpleMessageListener)); + #endregion + + private Message lastReceivedMessage; + private int messageCount; + + public Message LastReceivedMessage + { + get { return lastReceivedMessage; } + } + + + public int MessageCount + { + get { return messageCount; } + } + + #region IMessageListener Members + + public void OnMessage(Message message) + { + lastReceivedMessage = message; + messageCount++; + LOG.Debug("Message listener count = " + messageCount); + } + + #endregion + } +} \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleMessageListenerContainerTests.cs b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleMessageListenerContainerTests.cs new file mode 100644 index 00000000..7d608cb0 --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleMessageListenerContainerTests.cs @@ -0,0 +1,106 @@ +#region License + +/* + * Copyright © 2002-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +#region Imports + +using System; +using System.Messaging; +using System.Threading; +using NUnit.Framework; +using Spring.Messaging.Core; +using Spring.Testing.NUnit; + +#endregion + +namespace Spring.Messaging.Listener +{ + /// + /// This class contains tests for SimpleMessageListenerContainer + /// + /// Mark Pollack + /// $Id:$ + [TestFixture] + public class SimpleMessageListenerContainerTests : AbstractDependencyInjectionSpringContextTests + { + + + [Test, ExpectedException(typeof(ArgumentException), ExpectedMessage = "Property 'DefaultMessageQueue' is required")] + public void EnsureMessageQueuePropertyIsSet() + { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.AfterPropertiesSet(); + container.Start(); + } + + + [Test] + public void SendAndAsyncReceive() + { + + SimpleMessageListenerContainer container = + applicationContext["simpleMessageListenerContainer"] as SimpleMessageListenerContainer; + SimpleMessageListener listener = applicationContext["simpleMessageListener"] as SimpleMessageListener; + Assert.IsNotNull(container); + Assert.IsNotNull(listener); + + MessageQueueTemplate q = applicationContext["queue"] as MessageQueueTemplate; + Assert.IsNotNull(q); + q.ConvertAndSend("Hello World 1"); + + int waitInMillis = 2000; + Thread.Sleep(waitInMillis); + Assert.AreEqual(0, listener.MessageCount); + + container.Start(); + //pick up the message that is already in the queue + Thread.Sleep(waitInMillis); + Assert.AreEqual(1, listener.MessageCount); + + container.Stop(); + q.ConvertAndSend("Hello World 2"); + + //what happens to this message, we stopped, so no new event is fired. + Thread.Sleep(waitInMillis); + Assert.AreEqual(1, listener.MessageCount); + + container.Start(); + Thread.Sleep(waitInMillis); + //did we get hello world 2? + Assert.AreEqual(2, listener.MessageCount); + + /* + q.ConvertAndSend("Hello World"); + + Thread.Sleep(waitInMillis); + Assert.AreEqual(2, listener.MessageCount); + */ + container.Stop(); + container.Shutdown(); + Thread.Sleep(waitInMillis); + + } + + + protected override string[] ConfigLocations + { + get { return new string[] { "assembly://Spring.Messaging.Tests/Spring.Messaging.Listener/SimpleMessageListenerContainerTests.xml" }; } + } + } +} \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleMessageListenerContainerTests.xml b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleMessageListenerContainerTests.xml new file mode 100644 index 00000000..08b62103 --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/SimpleMessageListenerContainerTests.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Listener/TransactionalMessageListenerContainerTests.cs b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/TransactionalMessageListenerContainerTests.cs new file mode 100644 index 00000000..e68625a5 --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/TransactionalMessageListenerContainerTests.cs @@ -0,0 +1,135 @@ +#region License + +/* + * Copyright © 2002-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +#region Imports + +using System; +using System.Threading; +using NUnit.Framework; +using Spring.Messaging.Core; +using Spring.Testing.NUnit; + +#endregion + +namespace Spring.Messaging.Listener +{ + /// + /// This class contains integration tests for the TransactionalMessageListenerContainer + /// + /// Mark Pollack + [TestFixture] + public class TransactionalMessageListenerContainerTests : AbstractDependencyInjectionSpringContextTests + { + private int waitInMillis = 20000; + private TransactionalMessageListenerContainer transactionalMessageListenerContainer; + private SimpleHandler listener; + + public TransactionalMessageListenerContainer TransactionalMessageListenerContainer + { + set { transactionalMessageListenerContainer = value; } + } + + + public SimpleHandler SimpleHandler + { + set { listener = value; } + } + + [Test, ExpectedException(typeof(ArgumentException), ExpectedMessage = "Property 'DefaultMessageQueue' is required")] + public void EnsureMessageQueuePropertyIsSet() + { + TransactionalMessageListenerContainer container = new TransactionalMessageListenerContainer(); + container.AfterPropertiesSet(); + container.Start(); + } + + [Test] + public void EnsureuseContainerManagedMessageQueueTransactionIsSetCorrectly() + { + TransactionalMessageListenerContainer container = applicationContext["transactionalMessageListenerContainer"] as TransactionalMessageListenerContainer; + Assert.IsNotNull(container); + Assert.AreEqual(true, container.UseContainerManagedMessageQueueTransaction); + } + + [Test] + public void SendAndAsyncReceiveWithExceptionHandling() + { + MessageQueueTemplate q = applicationContext["queueTemplate"] as MessageQueueTemplate; + Assert.IsNotNull(q); + + MessageQueueTemplate retryQ = applicationContext["retryQueueTemplate"] as MessageQueueTemplate; + Assert.IsNotNull(retryQ); + + q.ConvertAndSend("Goodbye World 1"); + + Assert.AreEqual(0, listener.MessageCount); + transactionalMessageListenerContainer.Start(); + + Thread.Sleep(waitInMillis); + + + transactionalMessageListenerContainer.Stop(); + transactionalMessageListenerContainer.Shutdown(); + Thread.Sleep(2500); + + object msg = retryQ.ReceiveAndConvert(); + Assert.IsNotNull(msg); + string textMsg = msg as string; + Assert.IsNotNull(textMsg); + Assert.AreEqual("Goodbye World 1", textMsg); + } + + + [Test] + public void SendAndAsyncReceive() + { + + + MessageQueueTemplate q = applicationContext["queueTemplate"] as MessageQueueTemplate; + Assert.IsNotNull(q); + + q.ConvertAndSend("Hello World 1"); + q.ConvertAndSend("Hello World 2"); + q.ConvertAndSend("Hello World 3"); + q.ConvertAndSend("Hello World 4"); + q.ConvertAndSend("Hello World 5"); + + Assert.AreEqual(0, listener.MessageCount); + + transactionalMessageListenerContainer.Start(); + + Thread.Sleep(waitInMillis); + Assert.AreEqual(5, listener.MessageCount); + + transactionalMessageListenerContainer.Stop(); + transactionalMessageListenerContainer.Shutdown(); + Thread.Sleep(2500); + + } + + + protected override string[] ConfigLocations + { + get { return new string[] { "assembly://Spring.Messaging.Tests/Spring.Messaging.Listener/TransactionalMessageListenerContainerTests.xml" }; } + } + } + + +} \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Listener/TransactionalMessageListenerContainerTests.xml b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/TransactionalMessageListenerContainerTests.xml new file mode 100644 index 00000000..56d1fe27 --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Listener/TransactionalMessageListenerContainerTests.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Support/MessageAlreadyProcessedException.cs b/test/Spring/Spring.Messaging.Tests/Messaging/Support/MessageAlreadyProcessedException.cs new file mode 100644 index 00000000..f5653447 --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Support/MessageAlreadyProcessedException.cs @@ -0,0 +1,65 @@ +using System; +using System.Runtime.Serialization; + +namespace Spring.Messaging +{ + /// + /// Indicates that the received message has already been processed. Will typically be used + /// in an IMessageTransactionExceptionHandler to commit (i.e. remove from the queue) a + /// message that has been processed by the database but not acknowledged to MSMQ + /// due to an application failure. + /// + public class MessageAlreadyProcessedException : MessagingException + { + #region Constructor (s) / Destructor + + /// Creates a new instance of the MessageAlreadyProcessedException class. + public MessageAlreadyProcessedException() + { + } + + /// + /// Creates a new instance of the MessageAlreadyProcessedException class. with the specified message. + /// + /// + /// A message about the exception. + /// + public MessageAlreadyProcessedException(string message) : base(message) + { + } + + /// + /// Creates a new instance of the MessageAlreadyProcessedException class with the specified message + /// and root cause. + /// + /// + /// A message about the exception. + /// + /// + /// The root exception that is being wrapped. + /// + public MessageAlreadyProcessedException(string message, Exception rootCause) + : base(message, rootCause) + { + } + + /// + /// Creates a new instance of the MessageAlreadyProcessedException class. + /// + /// + /// The + /// that holds the serialized object data about the exception being thrown. + /// + /// + /// The + /// that contains contextual information about the source or destination. + /// + protected MessageAlreadyProcessedException( + SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + #endregion + } +} \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/Support/MessageQueueFactoryObjectTests.cs b/test/Spring/Spring.Messaging.Tests/Messaging/Support/MessageQueueFactoryObjectTests.cs new file mode 100644 index 00000000..eddd8faf --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/Support/MessageQueueFactoryObjectTests.cs @@ -0,0 +1,89 @@ +#region License + +/* + * Copyright © 2002-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +#region Imports + +using System.Messaging; +using NUnit.Framework; +using Spring.Context; +using Spring.Context.Support; +using Spring.Testing.NUnit; + +#endregion + +namespace Spring.Messaging.Support +{ + /// + /// This class contains tests for the MessageQueueFactory + /// + /// Mark Pollack + [TestFixture] + public class MessageQueueFactoryObjectTests : AbstractDependencyInjectionSpringContextTests + { + [Test] + public void CheckDefaultConstructorValues() + { + MessageQueueFactoryObject mqFactoryObject = new MessageQueueFactoryObject(); + MessageQueue queue = mqFactoryObject.GetObject() as MessageQueue; + Assert.IsNotNull(queue); + Assert.AreEqual(string.Empty, queue.Path); + Assert.AreEqual(false, queue.DenySharedReceive); + Assert.AreEqual(QueueAccessMode.SendAndReceive, queue.AccessMode); + //EnableCache property not on queue. + } + + [Test] + public void CheckSimpleProperties() + { + MessageQueueFactoryObject mqFactoryObject = (MessageQueueFactoryObject) applicationContext["&testqueue"]; + Assert.AreEqual(@".\Private$\testqueue", mqFactoryObject.Path); + Assert.AreEqual(true, mqFactoryObject.DenySharedReceive); + Assert.AreEqual(QueueAccessMode.Receive, mqFactoryObject.AccessMode); + Assert.AreEqual(true, mqFactoryObject.EnableCache); + MessageQueue mq = (MessageQueue) applicationContext["testqueue"]; + Assert.AreEqual("MyLabel", mq.Label); + } + + + [Test] + public void CheckGetObjectReturnsNewInstance() + { + MessageQueueFactoryObject mqFactoryObject = new MessageQueueFactoryObject(); + MessageQueue queue = mqFactoryObject.GetObject() as MessageQueue; + MessageQueue anotherQueue = mqFactoryObject.GetObject() as MessageQueue; + Assert.IsFalse(queue == anotherQueue, "Should be returning new instances"); + Assert.IsFalse(mqFactoryObject.IsSingleton, + "The MessageQueueFactoryObject class must be configured to return shared instances."); + } + + [Test] + public void ObjectTypePropertyYieldsTheCorrectType() + { + MessageQueueFactoryObject mqFactoryObject = new MessageQueueFactoryObject(); + Assert.AreEqual(typeof (MessageQueue), mqFactoryObject.ObjectType, + "The MessageQueueFactoryObject class ain't giving back DefaultMessageQueue types (it must)."); + } + + protected override string[] ConfigLocations + { + get { return new string[] {"assembly://Spring.Messaging.Tests/Spring.Messaging/queue-context.xml"}; } + } + } +} \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Messaging/queue-context.xml b/test/Spring/Spring.Messaging.Tests/Messaging/queue-context.xml new file mode 100644 index 00000000..85497d65 --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Messaging/queue-context.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Properties/AssemblyInfo.cs b/test/Spring/Spring.Messaging.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..2b5eba4c --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Spring.Messaging.Test")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Spring.Messaging.Test")] +[assembly: AssemblyCopyright("Copyright © 2008")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("75d6d9a9-9fcf-45ff-9691-1b0cc086e243")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/Spring/Spring.Messaging.Tests/Spring.Messaging.Tests.2005.csproj b/test/Spring/Spring.Messaging.Tests/Spring.Messaging.Tests.2005.csproj new file mode 100644 index 00000000..9693ff4a --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Spring.Messaging.Tests.2005.csproj @@ -0,0 +1,125 @@ + + + Debug + AnyCPU + 8.0.50727 + 2.0 + {41BC3AEA-7EB3-48BF-B1EC-84119376AC98} + Library + Properties + Spring + Spring.Messaging.Tests + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\..\..\lib\Net\2.0\Common.Logging.dll + + + False + ..\..\..\lib\Net\2.0\Common.Logging.Log4Net.dll + + + False + ..\..\..\lib\Net\2.0\log4net.dll + + + False + ..\..\..\lib\Net\2.0\nunit.framework.dll + + + False + ..\..\..\lib\Net\2.0\Rhino.Mocks.dll + + + + + + + + + + + + + + + {710961A3-0DF4-49E4-A26E-F5B9C044AC84} + Spring.Core.2005 + + + {AE00E5AB-C39A-436F-86D2-33BFE33E2E40} + Spring.Data.2005 + + + {0E23AE41-D8D8-41C2-84A2-D35564049F0D} + Spring.Messaging.2005 + + + {ED204A7B-832F-44C7-BFE3-504AEBE1BCC8} + Spring.Testing.NUnit.2005 + + + {44B16BAA-6DF8-447C-9D7F-3AD3D854D904} + Spring.Core.Tests.2005 + + + + + + + + + + + Always + + + + + + + + Code + + + + Code + + + Code + + + Code + + + Code + + + + + + \ No newline at end of file diff --git a/test/Spring/Spring.Messaging.Tests/Spring.Messaging.Tests.build b/test/Spring/Spring.Messaging.Tests/Spring.Messaging.Tests.build new file mode 100644 index 00000000..12cdae9b --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Spring.Messaging.Tests.build @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Spring/Spring.Messaging.Tests/Spring.Messaging.Tests.dll.config b/test/Spring/Spring.Messaging.Tests/Spring.Messaging.Tests.dll.config new file mode 100644 index 00000000..dce5b27c --- /dev/null +++ b/test/Spring/Spring.Messaging.Tests/Spring.Messaging.Tests.dll.config @@ -0,0 +1,68 @@ + + + + + + + +
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +