├── src ├── docs │ └── asciidoc │ │ ├── api │ │ └── .ignore │ │ ├── receivingMessages.adoc │ │ ├── logging.adoc │ │ ├── introduction.adoc │ │ ├── ref │ │ └── Service │ │ │ ├── sendJMSMessage.adoc │ │ │ ├── sendPubSubJMSMessage.adoc │ │ │ ├── sendQueueJMSMessage.adoc │ │ │ ├── sendTopicJMSMessage.adoc │ │ │ ├── receiveSelectedJMSMessage.adoc │ │ │ └── receiveSelectedAsyncJMSMessage.adoc │ │ ├── installation.adoc │ │ ├── sendingMessages │ │ ├── postProcessing.adoc │ │ └── usingOtherTemplates.adoc │ │ ├── receivingMessages │ │ ├── listenerReturnValues.adoc │ │ ├── serviceListeners.adoc │ │ ├── usingOtherContainersOrAdapters.adoc │ │ └── serviceMethodListeners.adoc │ │ ├── jmsProvider.adoc │ │ ├── configuration │ │ ├── changingDefaults.adoc │ │ └── syntaxNotes.adoc │ │ ├── messageConversion.adoc │ │ ├── configuration.adoc │ │ ├── disablingAndReloading.adoc │ │ ├── introduction │ │ └── springJms.adoc │ │ ├── jmsProvider │ │ └── activeMqExample.adoc │ │ ├── examples.adoc │ │ ├── sendingMessages.adoc │ │ ├── index.adoc │ │ ├── receivingMessagesWithSelectors │ │ └── receivingMethodsAddedToControllersAndServices.adoc │ │ ├── receivingMessagesWithSelectors.adoc │ │ └── browsingMessagesInQueue.adoc ├── main │ └── groovy │ │ └── grails │ │ └── plugin │ │ └── jms │ │ ├── Queue.java │ │ ├── Subscriber.java │ │ ├── listener │ │ ├── ListenerConfigFactory.groovy │ │ ├── GrailsMessagePostProcessor.groovy │ │ ├── adapter │ │ │ ├── PersistenceContextAwareListenerAdapter.groovy │ │ │ └── LoggingListenerAdapter.groovy │ │ ├── ListenerConfig.groovy │ │ └── ServiceInspector.groovy │ │ ├── bean │ │ ├── JmsTemplateBeanDefinitionBuilder.groovy │ │ ├── JmsMessageConverterBeanDefinitionBuilder.groovy │ │ ├── JmsListenerContainerAbstractBeanDefinitionBuilder.groovy │ │ ├── JmsListenerAdapterAbstractBeanDefinitionBuilder.groovy │ │ ├── JmsBeanDefinitionBuilder.groovy │ │ ├── DefaultJmsBeans.groovy │ │ ├── JmsBeanDefinitionsBuilder.groovy │ │ └── MapBasedBeanDefinitionBuilder.groovy │ │ ├── JmsGrailsPlugin.groovy │ │ └── connection │ │ └── JmsFallbackConnectionFactory.java └── test │ └── groovy │ └── grails │ └── plugin │ └── jms │ ├── bean │ ├── JmsListenerAdapterAbstractBeanDefinitionBuilderTests.groovy │ ├── JmsListenerContainerAbstractBeanDefinitionBuilderTests.groovy │ ├── JmsBeanDefinitionsBuilderTests.groovy │ ├── JmsBeanDefinitionBuilderTests.groovy │ └── MapBasedBeanDefinitionBuilderTests.groovy │ ├── listener │ ├── ListenerConfigTests.groovy │ └── ServiceInspectorSpec.groovy │ ├── JmsServiceConfSpec.groovy │ ├── JmsServiceAsyncExecutorSpec.groovy │ └── JmsGrailsPluginConfSpec.groovy ├── settings.gradle ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── grails-app ├── init │ └── jms │ │ └── Application.groovy ├── conf │ └── application.yml └── services │ └── grails │ └── plugin │ └── jms │ └── JmsService.groovy ├── README.md ├── .github └── workflows │ ├── stale.yml │ ├── build.yml │ ├── release_docs_only.yml │ └── release.yml ├── gradlew.bat └── gradlew /src/docs/asciidoc/api/.ignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'jms' -------------------------------------------------------------------------------- /src/docs/asciidoc/receivingMessages.adoc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .gradle 3 | *.iml 4 | .idea 5 | /target 6 | *.gpg 7 | 8 | -------------------------------------------------------------------------------- /src/docs/asciidoc/logging.adoc: -------------------------------------------------------------------------------- 1 | All logging is done under the namespace 'grails.plugin.jms'. -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gpc/jms/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Thu, 15 Feb 2024 14:45:56 +0000 2 | version=4.0.3-SNAPSHOT 3 | grailsVersion=5.2.5 4 | grailsGradleVersion=5.2.4 5 | -------------------------------------------------------------------------------- /src/docs/asciidoc/introduction.adoc: -------------------------------------------------------------------------------- 1 | 2 | This plugin makes it easy to both send and receive JMS messages inside a Grails application. 3 | 4 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/docs/asciidoc/ref/Service/sendJMSMessage.adoc: -------------------------------------------------------------------------------- 1 | ==== sendJMSMessage 2 | 3 | ==== Purpose 4 | 5 | ==== Examples 6 | 7 | [source,java] 8 | ---- 9 | foo.sendJMSMessage(object,object) 10 | ---- 11 | 12 | ==== Description 13 | 14 | Arguments: 15 | 16 | [* `object` 17 | , * `object` 18 | ] 19 | -------------------------------------------------------------------------------- /src/docs/asciidoc/ref/Service/sendPubSubJMSMessage.adoc: -------------------------------------------------------------------------------- 1 | === sendPubSubJMSMessage 2 | 3 | ==== Purpose 4 | 5 | ==== Examples 6 | 7 | [source,java] 8 | ---- 9 | foo.sendPubSubJMSMessage(object,object) 10 | ---- 11 | 12 | ==== Description 13 | 14 | Arguments: 15 | 16 | [* `object` 17 | , * `object` 18 | ] 19 | -------------------------------------------------------------------------------- /src/docs/asciidoc/ref/Service/sendQueueJMSMessage.adoc: -------------------------------------------------------------------------------- 1 | ==== sendQueueJMSMessage 2 | 3 | ==== Purpose 4 | 5 | ==== Examples 6 | 7 | [source,java] 8 | ---- 9 | foo.sendQueueJMSMessage(object,object) 10 | ---- 11 | 12 | ==== Description 13 | 14 | Arguments: 15 | 16 | [* `object` 17 | , * `object` 18 | ] 19 | -------------------------------------------------------------------------------- /src/docs/asciidoc/ref/Service/sendTopicJMSMessage.adoc: -------------------------------------------------------------------------------- 1 | ==== sendTopicJMSMessage 2 | 3 | ==== Purpose 4 | 5 | ==== Examples 6 | 7 | [source,java] 8 | ---- 9 | foo.sendTopicJMSMessage(object,object) 10 | ---- 11 | 12 | ==== Description 13 | 14 | Arguments: 15 | 16 | [* `object` 17 | , * `object` 18 | ] 19 | -------------------------------------------------------------------------------- /src/docs/asciidoc/ref/Service/receiveSelectedJMSMessage.adoc: -------------------------------------------------------------------------------- 1 | ==== receiveSelectedJMSMessage 2 | 3 | ==== Purpose 4 | 5 | ==== Examples 6 | 7 | [source,java] 8 | ---- 9 | foo.receiveSelectedJMSMessage(object,object) 10 | ---- 11 | 12 | ==== Description 13 | 14 | Arguments: 15 | 16 | [* `object` 17 | , * `object` 18 | ] 19 | -------------------------------------------------------------------------------- /src/docs/asciidoc/installation.adoc: -------------------------------------------------------------------------------- 1 | 2 | The plugin is available on Maven Central and should be a dependency like this: 3 | 4 | [source,groovy,subs="attributes"] 5 | ---- 6 | dependencies { 7 | implementation 'io.github.gpc:jms:{version}' 8 | } 9 | ---- 10 | 11 | In older versions of Gradle, replace `implementation` with `compile` 12 | -------------------------------------------------------------------------------- /src/docs/asciidoc/ref/Service/receiveSelectedAsyncJMSMessage.adoc: -------------------------------------------------------------------------------- 1 | ==== receiveSelectedAsyncJMSMessage 2 | 3 | ==== Purpose 4 | 5 | ==== Examples 6 | 7 | [source,java] 8 | ---- 9 | foo.receiveSelectedAsyncJMSMessage(object,object) 10 | ---- 11 | 12 | ==== Description 13 | 14 | Arguments: 15 | 16 | [* `object` 17 | , * `object` 18 | ] 19 | -------------------------------------------------------------------------------- /grails-app/init/jms/Application.groovy: -------------------------------------------------------------------------------- 1 | package jms 2 | 3 | import grails.boot.GrailsApp 4 | import grails.boot.config.GrailsAutoConfiguration 5 | import grails.plugins.metadata.PluginSource 6 | 7 | @PluginSource 8 | class Application extends GrailsAutoConfiguration { 9 | static void main(String[] args) { 10 | GrailsApp.run(Application, args) 11 | } 12 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/gpc/jms/actions/workflows/build.yml/badge.svg)](https://github.com/gpc/jms/actions/workflows/build.yml) [![Version](https://badgen.net/github/tag/gpc/jms)](https://github.com/gpc/jms/releases) 2 | 3 | JMS for Grails 4 | ============== 5 | This branch is for Grails 5.x 6 | 7 | JMS integration for Grails. See [documentation](http://gpc.github.io/jms/latest/) for more information. 8 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/Queue.java: -------------------------------------------------------------------------------- 1 | package grails.plugin.jms; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | 7 | @Documented 8 | @Retention(RetentionPolicy.RUNTIME) 9 | public @interface Queue { 10 | String container() default "standard"; 11 | String adapter() default "standard"; 12 | String name() default ""; 13 | String selector() default ""; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/Subscriber.java: -------------------------------------------------------------------------------- 1 | package grails.plugin.jms; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | 7 | @Documented 8 | @Retention(RetentionPolicy.RUNTIME) 9 | public @interface Subscriber { 10 | String container() default "standard"; 11 | String adapter() default "standard"; 12 | String topic() default ""; 13 | String selector() default ""; 14 | } 15 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugin/jms/bean/JmsListenerAdapterAbstractBeanDefinitionBuilderTests.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.jms.bean 2 | 3 | import grails.spring.BeanBuilder 4 | 5 | class JmsListenerAdapterAbstractBeanDefinitionBuilderTests extends GroovyTestCase { 6 | 7 | private bb = new BeanBuilder() 8 | 9 | void testCreate() { 10 | def bdb = new JmsListenerAdapterAbstractBeanDefinitionBuilder('example', [:]) 11 | bdb.build(bb) 12 | assertTrue(bb.getBeanDefinition(bdb.name).'abstract') 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugin/jms/bean/JmsListenerContainerAbstractBeanDefinitionBuilderTests.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.jms.bean 2 | 3 | import grails.spring.BeanBuilder 4 | 5 | class JmsListenerContainerAbstractBeanDefinitionBuilderTests extends GroovyTestCase { 6 | 7 | private bb = new BeanBuilder() 8 | 9 | void testCreate() { 10 | def bdb = new JmsListenerContainerAbstractBeanDefinitionBuilder('example', [:]) 11 | bdb.build(bb) 12 | assertTrue(bb.getBeanDefinition(bdb.name).'abstract') 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: '00 06 * * 1' 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | pull-requests: write 14 | 15 | steps: 16 | - uses: actions/stale@v3 17 | with: 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | stale-issue-message: 'This issue looks like it is stale and therefor it is in risk of being closed with no further action.' 20 | stale-pr-message: 'This pull request looks like it is stale and therefor it is in risk of being closed with no further action.' 21 | stale-issue-label: 'no-issue-activity' 22 | stale-pr-label: 'no-pr-activity' 23 | days-before-stale: 180 24 | -------------------------------------------------------------------------------- /src/docs/asciidoc/sendingMessages/postProcessing.adoc: -------------------------------------------------------------------------------- 1 | Message post processors can either augment the passed Message object, or create a new one. 2 | Because of this, the post processor must return the message object that is to be sent. 3 | 4 | [source,groovy] 5 | ---- 6 | import javax.jms.Message 7 | 8 | jmsService.send(topic: 'somethingHappened', 1) { Message msg -> 9 | msg.JMSCorrelationID = "correlate" 10 | msg 11 | } 12 | ---- 13 | 14 | === Setting destinations 15 | 16 | Post processors can use the `createDestination()` method in post processor implementations to create destinations using the same API style as `jmsService.send()` method 17 | 18 | [source,groovy] 19 | ---- 20 | jmsService.send(service: 'initial', 1) { 21 | it.JMSReplyTo = createDestination(service: 'reply') 22 | it 23 | } 24 | ---- 25 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 8 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: '8' 23 | distribution: 'adopt' 24 | cache: gradle 25 | - name: Grant execute permission for gradlew 26 | run: chmod +x gradlew 27 | - name: Build with Gradle 28 | run: ./gradlew build 29 | -------------------------------------------------------------------------------- /src/docs/asciidoc/receivingMessages/listenerReturnValues.adoc: -------------------------------------------------------------------------------- 1 | Spring's http://static.springsource.org/spring/docs/3.0.x/api/org/springframework/jms/listener/adapter/MessageListenerAdapter.html[MessageListenerAdapter] adds some special handling of listener method return values. 2 | 3 | From MessageListenerAdapter's JavaDoc: "If a target listener method returns a non-null object (typically of a message content type such as String or byte array), it will get wrapped in a JMS Message and sent to the response destination (either the JMS "reply-to" destination or a specified default destination)." 4 | 5 | Be careful with Groovy's implicit return mechanism; ensure that you return null explicitly if you want nothing to be sent to the reply destination. 6 | If you accidentally return a value that cannot be sent to the reply destination, you may have odd side effects like messages never being removed from the queue (due to implicit rollbacks!). 7 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugin/jms/bean/JmsBeanDefinitionsBuilderTests.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.jms.bean 2 | 3 | import grails.spring.BeanBuilder 4 | 5 | class JmsBeanDefinitionsBuilderTests extends GroovyTestCase { 6 | 7 | void testIt() { 8 | 9 | def bb = new BeanBuilder(getClass().classLoader) 10 | 11 | def beans = [ 12 | converters: [standard: [:]], 13 | templates: [ 14 | standard: [ 15 | meta: ['abstract': true] 16 | ] 17 | ], 18 | containers: [ 19 | standard: [:] 20 | ], 21 | adapters: [ 22 | standard: [:] 23 | ] 24 | ] 25 | 26 | new JmsBeanDefinitionsBuilder(beans).build(bb) 27 | 28 | JmsBeanDefinitionsBuilder.mappings.each { key, builderClazz -> 29 | assertNotNull(bb.getBeanDefinition('standard' + builderClazz.nameSuffix)) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/listener/ListenerConfigFactory.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Grails Plugin Collective 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package grails.plugin.jms.listener 17 | 18 | import grails.util.GrailsNameUtils 19 | 20 | class ListenerConfigFactory { 21 | 22 | def getListenerConfig(Class serviceClass, grailsApplication) { 23 | new ListenerConfig( 24 | serviceBeanName: GrailsNameUtils.getPropertyName(serviceClass), 25 | grailsApplication: grailsApplication 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/bean/JmsTemplateBeanDefinitionBuilder.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Grails Plugin Collective 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package grails.plugin.jms.bean 17 | 18 | import org.springframework.jms.core.JmsTemplate 19 | 20 | class JmsTemplateBeanDefinitionBuilder extends JmsBeanDefinitionBuilder { 21 | 22 | static final String nameSuffix = "JmsTemplate" 23 | static final Class defaultClazz = JmsTemplate 24 | 25 | JmsTemplateBeanDefinitionBuilder(name, definition) { 26 | super(name, definition) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugin/jms/bean/JmsBeanDefinitionBuilderTests.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.jms.bean 2 | 3 | import grails.spring.BeanBuilder 4 | 5 | class JmsBeanDefinitionBuilderTests extends GroovyTestCase { 6 | 7 | private bb = new BeanBuilder() 8 | 9 | void testCreate() { 10 | def nameBase = "example" 11 | 12 | def bdb = new JmsBeanDefinitionBuilderTestsTestImpl(nameBase, [a: "a"]) 13 | 14 | assertEquals(nameBase + JmsBeanDefinitionBuilderTestsTestImpl.nameSuffix, bdb.name) 15 | assertEquals(JmsBeanDefinitionBuilderTestsTestImpl.defaultClazz, bdb.clazz) 16 | } 17 | 18 | void testExplicitClass() { 19 | def nameBase = "example" 20 | 21 | def bdb = new JmsBeanDefinitionBuilderTestsTestImpl(nameBase, [a: "a", clazz: String]) 22 | 23 | assertEquals(String, bdb.clazz) 24 | } 25 | } 26 | 27 | class JmsBeanDefinitionBuilderTestsTestImpl extends JmsBeanDefinitionBuilder { 28 | 29 | JmsBeanDefinitionBuilderTestsTestImpl(name, definition) { 30 | super(name, definition) 31 | } 32 | 33 | static final String nameSuffix = "Suffix" 34 | static final Class defaultClazz = [:].getClass() 35 | } 36 | -------------------------------------------------------------------------------- /src/docs/asciidoc/jmsProvider.adoc: -------------------------------------------------------------------------------- 1 | The plugin does not include a JMS provider so you must install and configure your own. 2 | 3 | All you need to provide is one or more http://java.sun.com/javaee/5/docs/api/javax/jms/ConnectionFactory.html[javax.jms.ConnectionFactory] beans and the plugin takes care of the rest. 4 | 5 | The plugin looks for a connection factory bean named `jmsConnectionFactory`. 6 | 7 | The default SpringBoot configuration has caching enabled, which results in a bean named `cachingJmsConnectionFactory` being defined rather than `jmsConnectionFactory`. 8 | 9 | To turn caching off, set the configuration property `spring.jms.cache.enabled` in `application.yml` like so. 10 | 11 | [source,groovy] 12 | ---- 13 | spring: 14 | jms: 15 | cache: 16 | enabled: false 17 | ---- 18 | 19 | With caching disabled, there will be a `jmsConnectionFactory` bean defined. 20 | 21 | If you wish to use this plugin with caching enabled, you can add the following line in `resources.groovy` to use a spring bean alias. 22 | 23 | [source,groovy] 24 | ---- 25 | beans = { 26 | 27 | // ... 28 | springConfig.addAlias('jmsConnectionFactory', 'cachingJmsConnectionFactory') 29 | } 30 | ---- -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/bean/JmsMessageConverterBeanDefinitionBuilder.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Grails Plugin Collective 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package grails.plugin.jms.bean 17 | 18 | import org.springframework.jms.support.converter.SimpleMessageConverter 19 | 20 | class JmsMessageConverterBeanDefinitionBuilder extends JmsBeanDefinitionBuilder { 21 | 22 | static final String nameSuffix = "JmsMessageConverter" 23 | static final Class defaultClazz = SimpleMessageConverter 24 | 25 | JmsMessageConverterBeanDefinitionBuilder(name, definition) { 26 | super(name, definition) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/bean/JmsListenerContainerAbstractBeanDefinitionBuilder.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Grails Plugin Collective 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package grails.plugin.jms.bean 17 | 18 | import org.springframework.jms.listener.DefaultMessageListenerContainer 19 | 20 | class JmsListenerContainerAbstractBeanDefinitionBuilder extends JmsBeanDefinitionBuilder { 21 | 22 | static final String nameSuffix = "JmsListenerContainer" 23 | static final Class defaultClazz = DefaultMessageListenerContainer 24 | 25 | JmsListenerContainerAbstractBeanDefinitionBuilder(name, definition) { 26 | super(name, definition) 27 | } 28 | 29 | def getMeta() { 30 | (super.getMeta() ?: [:]) + ['abstract': true] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/bean/JmsListenerAdapterAbstractBeanDefinitionBuilder.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Grails Plugin Collective 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package grails.plugin.jms.bean 17 | 18 | import grails.plugin.jms.listener.adapter.PersistenceContextAwareListenerAdapter 19 | 20 | class JmsListenerAdapterAbstractBeanDefinitionBuilder extends JmsBeanDefinitionBuilder { 21 | 22 | static final String nameSuffix = "JmsListenerAdapter" 23 | static final Class defaultClazz = PersistenceContextAwareListenerAdapter 24 | 25 | JmsListenerAdapterAbstractBeanDefinitionBuilder(name, definition) { 26 | super(name, definition) 27 | } 28 | 29 | def getMeta() { 30 | (super.getMeta() ?: [:]) + ['abstract': true] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/docs/asciidoc/sendingMessages/usingOtherTemplates.adoc: -------------------------------------------------------------------------------- 1 | Here is an example of using a custom template that uses a different connection factory. 2 | 3 | === Example 4 | 5 | ==== resources.groovy 6 | 7 | [source,groovy] 8 | ---- 9 | import org.apache.activemq.ActiveMQConnectionFactory 10 | import org.springframework.jms.connection.SingleConnectionFactory 11 | 12 | beans = { 13 | // used by the standard template by convention 14 | jmsConnectionFactory(SingleConnectionFactory) { 15 | targetConnectionFactory = { ActiveMQConnectionFactory cf -> 16 | brokerURL = 'vm://localhost' 17 | } 18 | } 19 | 20 | otherJmsConnectionFactory(SingleConnectionFactory) { 21 | targetConnectionFactory = { ActiveMQConnectionFactory cf -> 22 | brokerURL = // ... something else 23 | } 24 | } 25 | } 26 | ---- 27 | 28 | ==== application.yml 29 | 30 | [source,groovy] 31 | ---- 32 | jms: 33 | templates: 34 | other: 35 | meta: 36 | parentBean: standardJmsTemplate 37 | connectionFactoryBean: otherJmsConnectionFactory // use different connection factory 38 | ---- 39 | 40 | ==== Sending messages 41 | 42 | [source,groovy] 43 | ---- 44 | jmsService.send(topic: "stuffHappened", message, "other") 45 | ---- 46 | 47 | The third argument of "other" to the send() method specifies to use the "other" template. 48 | -------------------------------------------------------------------------------- /src/docs/asciidoc/configuration/changingDefaults.adoc: -------------------------------------------------------------------------------- 1 | You can override the configuration defaults very easily. 2 | 3 | Let's suppose you do not want any message conversion on listeners. 4 | If a listener container has no `messageConverter` listeners will receive raw messages. 5 | So we want to override the standard listener container definition to set the `messageConverter` property to `null`. 6 | 7 | In your application's `application.yml` 8 | 9 | [source,groovy] 10 | ---- 11 | jms: 12 | containers: 13 | standard: 14 | messageConverter: null 15 | ---- 16 | 17 | This definition will get merged against the plugin provided defaults to produce a standard listener container definition with `messageConverter` set to `null`. 18 | 19 | 20 | == Disabling the default dependency on the Persistence Interceptor. 21 | 22 | If you are not using any *GORM* implementation such as *Grails Hibernate Plugin* (i.e. you uninstalled the *hibernate* plugin) or the *GORM* implementation you are using doesn't provide a **Persistence Interceptor Bean**, you will have to disable the _default_ dependency to the **Persistence Interceptor Bean**. 23 | You can do this by setting in the `application.yml` the `jms.adapters.standard.persistenceInterceptorBean` to `null` . 24 | 25 | [source,groovy] 26 | ---- 27 | jms: 28 | adapters: 29 | standard: 30 | persistenceInterceptorBean: null 31 | ---- 32 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/bean/JmsBeanDefinitionBuilder.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Grails Plugin Collective 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package grails.plugin.jms.bean 17 | 18 | abstract class JmsBeanDefinitionBuilder extends MapBasedBeanDefinitionBuilder { 19 | 20 | JmsBeanDefinitionBuilder(name, definition) { 21 | super(name, definition) 22 | } 23 | 24 | def getName() { 25 | super.getName() + getClass().nameSuffix 26 | } 27 | 28 | def getClazz() { 29 | super.getClazz() ?: getClass().defaultClazz 30 | } 31 | 32 | static getNameSuffix() { 33 | throw new IllegalStateException("${this} does not implement getNameSuffix()") 34 | } 35 | 36 | static getDefaultClazz() { 37 | throw new IllegalStateException("${this} does not implement getDefaultClazz()") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/docs/asciidoc/messageConversion.adoc: -------------------------------------------------------------------------------- 1 | Both templates and adapters use a http://static.springsource.org/spring/docs/current/javadoc-api/org/springframework/jms/support/converter/MessageConverter.html[MessageConverter] to convert objects into messages. 2 | By default, this plugin configures templates and adapters to use a http://static.springsource.org/spring/docs/current/javadoc-api/org/springframework/jms/support/converter/SimpleMessageConverter.html.[SimpleMessageConverter] 3 | This can be changed via the config mechanism… 4 | 5 | [source,groovy] 6 | ---- 7 | jms { 8 | converters { 9 | other { 10 | meta { 11 | clazz = my.custom.MessageConverter 12 | } 13 | } 14 | } 15 | adapters { 16 | other { 17 | meta { 18 | parentBean = 'standardJmsListenerAdapter' 19 | } 20 | messageConverterBean = "otherJmsMessageConverter" 21 | } 22 | } 23 | } 24 | ---- 25 | 26 | This would configure the “other” listener adapter to use our special message converter. 27 | 28 | To globally use a custom message converter, you can augment the standard definition… 29 | 30 | [source,groovy] 31 | ---- 32 | jms { 33 | converters { 34 | standard { 35 | meta { 36 | clazz = my.custom.MessageConverter 37 | } 38 | } 39 | } 40 | } 41 | ---- 42 | 43 | This would cause all templates and adapters to use your custom converter. 44 | -------------------------------------------------------------------------------- /src/docs/asciidoc/configuration.adoc: -------------------------------------------------------------------------------- 1 | JMS is a complicated topic. 2 | There are different consumption and configuration patterns. 3 | While this plugin does set some reasonable defaults, it's very likely that you are going to need to customise these settings either globally or for specific senders or listeners. 4 | 5 | To support this, the plugin makes configuration options available to you should you need to set it. 6 | This is achieved through the use of Spring's abstract beans and Grails' configuration mechanism. 7 | 8 | 9 | == How it works 10 | 11 | The configuration is controlled by the Grails application configuration under the key `jms`. 12 | This is merged against plugin provided defaults. 13 | 14 | Here is what the defaults look like... 15 | 16 | [source,groovy] 17 | ---- 18 | templates: 19 | standard: 20 | connectionFactoryBean: jmsConnectionFactory 21 | messageConverterBean: standardJmsMessageConverter 22 | 23 | ... 24 | ---- 25 | 26 | That creates a map of "bean definitions" that get processed into real bean definitions. 27 | 28 | The default config creates our standard (i.e. default) converters, jms templates for sending, and listener containers and adapters for receiving. 29 | 30 | When sending messages with the `jmsService` you can specify which template to use to send the message. 31 | If none is specified, "standard" is used. 32 | 33 | Likewise, listeners can specify which container and/or adapter bean definition to base themselves on. 34 | If none are specified, "standard" is used in both cases. 35 | -------------------------------------------------------------------------------- /src/docs/asciidoc/receivingMessages/serviceListeners.adoc: -------------------------------------------------------------------------------- 1 | == Service Listeners 2 | 3 | Service listeners are a convenient way to define one handler for JMS messages. 4 | The simplest service listener looks like… 5 | 6 | [source,java] 7 | ---- 8 | class PersonService { 9 | static exposes = ["jms"] 10 | def onMessage(msg) { 11 | // handle message 12 | } 13 | } 14 | ---- 15 | 16 | This will register the `onMessage` method as a listener for the JMS _queue_ named `«application name».person` , where «application name» is the `app.name` key from the `application.properties` file. 17 | 18 | 19 | 20 | === Configuration 21 | 22 | The following configuration parameters can be set as static variables on the service class… 23 | 24 | [format="csv",options="header"] 25 | |== 26 | 27 | Property Name,Type,Default,Description *destination*,String,«app name».«service name»,The named destination of the listener *isTopic*,boolean,false,is the destination a topic ( `true` ) or a queue ( `false` ) *selector*,String,null,See the “Message Selector” section of http://java.sun.com/j2ee/1.4/docs/api/javax/jms/Message.html 28 | *adapter*,String,"standard",The adapter to use for this listener *container*,String,"standard",The container to use for this listener |== 29 | 30 | [source,java] 31 | ---- 32 | class PersonService { 33 | static exposes = ["jms"] 34 | static destination = "somethingHappened" 35 | static isTopic = true 36 | static adapter = "custom" 37 | 38 | def onMessage(msg) { 39 | // handle message 40 | } 41 | } 42 | ---- 43 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/bean/DefaultJmsBeans.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.jms.bean 2 | 3 | /* 4 | * Copyright 2010 Grails Plugin Collective 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | import org.springframework.jms.listener.DefaultMessageListenerContainer 19 | 20 | converters { 21 | standard {} 22 | } 23 | templates { 24 | standard { 25 | connectionFactoryBean = "jmsConnectionFactory" 26 | messageConverterBean = "standardJmsMessageConverter" 27 | } 28 | } 29 | containers { 30 | standard { 31 | concurrentConsumers = 1 32 | subscriptionDurable = false 33 | autoStartup = false 34 | connectionFactoryBean = "jmsConnectionFactory" 35 | messageSelector = null 36 | cacheLevel = DefaultMessageListenerContainer.CACHE_CONSUMER 37 | } 38 | } 39 | adapters { 40 | standard { 41 | messageConverterBean = "standardJmsMessageConverter" 42 | persistenceInterceptorBean = 'persistenceInterceptor' 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/docs/asciidoc/configuration/syntaxNotes.adoc: -------------------------------------------------------------------------------- 1 | There are some noteworthy things about this config syntax. 2 | 3 | 4 | === Bean names 5 | 6 | The beans created automatically get suffixes applied to them. 7 | Template bean names get suffixed with 'JmsTemplate', container beans get suffixed with 'JmsListenerContainer' and adapter beans get suffixed with 'JmsListenerAdapter'. 8 | 9 | === Setting Beans 10 | 11 | To set a property to another Spring bean, simply append `Bean` to the property name and set the property to the name of the bean. 12 | 13 | Here is how the standard template is defined to use the bean named `jmsConnectionFactory` as it's connection factory... 14 | 15 | [source,groovy] 16 | ---- 17 | templates { 18 | standard { 19 | connectionFactoryBean = "jmsConnectionFactory" 20 | } 21 | } 22 | ---- 23 | 24 | === Setting Class 25 | 26 | To set the class of a bean, you must use the following syntax 27 | 28 | [source,groovy] 29 | ---- 30 | templates { 31 | standard { 32 | meta { 33 | clazz = my.org.CustomJmsTemplate 34 | } 35 | } 36 | } 37 | ---- 38 | 39 | === Extending Definitions 40 | 41 | Bean definition can inherit from parents and selectively override settings. 42 | 43 | [source,groovy] 44 | ---- 45 | templates { 46 | other { 47 | meta { 48 | parentBean = 'standardJmsTemplate' 49 | } 50 | connectionFactoryBean = "someOtherJmsConnectionFactory" 51 | } 52 | } 53 | ---- 54 | 55 | This creates an "other" template, that inherits all of the standard settings but uses a custom connectionFactory. 56 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugin/jms/bean/MapBasedBeanDefinitionBuilderTests.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.jms.bean 2 | 3 | import grails.spring.BeanBuilder 4 | import org.junit.* 5 | import static org.junit.Assert.* 6 | 7 | class MapBasedBeanDefinitionBuilderTests { 8 | 9 | private bb = new BeanBuilder() 10 | 11 | @Test 12 | void testCreate() { 13 | 14 | def s1Value = "s1Value" 15 | def s2Value = "s2Value" 16 | def i1Value = 1 17 | 18 | bb.parent(MapBasedBeanDefinitionBuilderTestsTestBean) { 19 | it.'abstract' = true 20 | s2 = s2Value 21 | } 22 | 23 | bb.s1(String, s1Value) 24 | 25 | def definition = [ 26 | meta: [ 27 | parentBean: "parent" 28 | ], 29 | clazz: MapBasedBeanDefinitionBuilderTestsTestBean, 30 | s1Bean: "s1", 31 | i1: i1Value 32 | ] 33 | 34 | def jbd = new MapBasedBeanDefinitionBuilder("test", definition) 35 | jbd.build(bb) 36 | 37 | def bd = bb.getBeanDefinition("test") 38 | assertNotNull(bd) 39 | 40 | assertEquals(MapBasedBeanDefinitionBuilderTestsTestBean.name, bd.beanClassName) 41 | assertEquals("parent", bd.parentName) 42 | 43 | def ac = bb.createApplicationContext() 44 | 45 | def testBean = ac.getBean('test') 46 | assertNotNull(testBean) 47 | 48 | assertEquals(s1Value, testBean.s1) 49 | assertEquals(s2Value, testBean.s2) 50 | assertEquals(i1Value, testBean.i1) 51 | } 52 | } 53 | 54 | class MapBasedBeanDefinitionBuilderTestsTestBean { 55 | String s1 56 | String s2 57 | Integer i1 58 | } 59 | -------------------------------------------------------------------------------- /src/docs/asciidoc/disablingAndReloading.adoc: -------------------------------------------------------------------------------- 1 | == Disabling 2 | 3 | You can globally disable all JMS functionality by setting `jms.disabled` to true in your application config. 4 | 5 | For example, you could turn JMS for testing with: 6 | 7 | [source,groovy] 8 | ---- 9 | environments { 10 | test { 11 | jms.disabled = true 12 | } 13 | } 14 | ---- 15 | 16 | If JMS is disabled then no listeners are registered (so no messages will be received). 17 | 18 | If an attempt is made to send a message while JMS is disabled you will only get a log message alerting you that the message will not be sent because JMS is disabled. 19 | This allows you to still use the `sendMessage()` methods or `jmsService` even if JMS is disabled. 20 | 21 | == Reloading 22 | 23 | The JMS plugin has good support for hot reloading during development. 24 | 25 | 26 | === Listeners 27 | 28 | If you make a change to a service class that is a listener during development, all existing listeners for that service will be shutdown. 29 | The service is then re-inspected for listeners that are then registered. 30 | This means you can change listener config and have it take effect without restarting your application. 31 | 32 | === Config 33 | 34 | If any change to the JMS config is detected, all JMS functionality is torn down and then re-established with the new config. 35 | This allows you to change bean definitions (such as container or template options) and have them take effect without restarting your application. 36 | 37 | === Disabled/Enabled 38 | 39 | You can also temporarily disable or enable JMS functionality by changing the `jms.disabled` config option during development and have it take effect without restarting your application. 40 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/bean/JmsBeanDefinitionsBuilder.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Grails Plugin Collective 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package grails.plugin.jms.bean 17 | 18 | class JmsBeanDefinitionsBuilder { 19 | 20 | static mappings = [ 21 | templates: JmsTemplateBeanDefinitionBuilder, 22 | containers: JmsListenerContainerAbstractBeanDefinitionBuilder, 23 | adapters: JmsListenerAdapterAbstractBeanDefinitionBuilder, 24 | converters: JmsMessageConverterBeanDefinitionBuilder 25 | ] 26 | 27 | final beans 28 | 29 | JmsBeanDefinitionsBuilder(beans) { 30 | this.beans = beans 31 | } 32 | 33 | def build(beanBuilder) { 34 | mappings.each { key, builderClazz -> 35 | beans[key].each { name, definition -> 36 | builderClazz.newInstance(name, definition).build(beanBuilder) 37 | } 38 | } 39 | } 40 | 41 | def removeFrom(context) { 42 | mappings.each { key, builderClazz -> 43 | beans[key].each { name, definition -> 44 | builderClazz.newInstance(name, definition).removeFrom(context) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/listener/GrailsMessagePostProcessor.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Grails Plugin Collective 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package grails.plugin.jms.listener 17 | 18 | import javax.jms.Message 19 | 20 | import org.springframework.jms.core.MessagePostProcessor 21 | 22 | class GrailsMessagePostProcessor implements MessagePostProcessor { 23 | 24 | def jmsTemplate 25 | def jmsService 26 | def processor 27 | 28 | def createDestination(destination) { 29 | def destinationMap = jmsService.convertToDestinationMap(destination) 30 | def session = jmsTemplate.createSession(jmsTemplate.createConnection()) 31 | def destinationResolver = jmsTemplate.destinationResolver 32 | def isTopic = destinationMap.containsKey("topic") 33 | def destinationString = (isTopic) ? destinationMap.topic : destinationMap.queue 34 | destinationResolver.resolveDestinationName(session, destinationString, isTopic) 35 | } 36 | 37 | Message postProcessMessage(Message message) { 38 | processor.delegate = this 39 | processor.resolveStrategy = Closure.DELEGATE_ONLY 40 | processor.call(message) ?: message 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/docs/asciidoc/introduction/springJms.adoc: -------------------------------------------------------------------------------- 1 | This plugin is built on top of http://static.springsource.org/spring/docs/3.0.x/reference/html/jms.html[Spring's JMS support] 2 | There are some core Spring JMS concepts that you should at least be aware of. 3 | 4 | 5 | == JmsTemplate 6 | 7 | Spring provides http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/jms/core/JmsTemplate.html[JmsTemplate] which is what this plugin uses to send messages. 8 | 9 | == MessageConverter 10 | 11 | The http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/jms/support/converter/MessageConverter.html[MessageConverter] abstraction conveniently allows pluggable message conversion. 12 | By default, this plugin uses Spring's http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/jms/support/converter/SimpleMessageConverter.html[SimpleMessageConverter] which handles 'standard' message payloads and JMS Message types. 13 | 14 | == MessageListenerContainer 15 | 16 | A listener container polls a JMS destination for messages. 17 | Each listener (i.e. each service method that receives JMS messages) has its own listener container. 18 | 19 | This plugin uses the http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/jms/listener/DefaultMessageListenerContainer.html[DefaultMessageListenerContainer] implementation. 20 | 21 | == MessageListenerAdapter 22 | 23 | A listener adapter connects a listener container to the actual destination of the message. 24 | It handles message conversion amongst other things. 25 | 26 | By default, this plugin uses a http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/jms/listener/adapter/MessageListenerAdapter.html[MessageListenerAdapter] subclass that is Grails aware and sets up the necessary Grails environment for listener methods (e.g. Hibernate session). 27 | -------------------------------------------------------------------------------- /src/docs/asciidoc/jmsProvider/activeMqExample.adoc: -------------------------------------------------------------------------------- 1 | Getting ActiveMQ up and running as your provider is very simple. 2 | All that is required is adding compile dependencies on the `spring-boot-starter-activemq` library and `pooled-jms` library in `build.gradle`. 3 | 4 | [source,groovy,subs='attributes'] 5 | ---- 6 | // ... 7 | 8 | dependencies { 9 | 10 | // ... 11 | compile 'io.github.gpc:jms:{version}' 12 | compile 'org.springframework.boot:spring-boot-starter-activemq' 13 | compile 'org.messaginghub:pooled-jms' 14 | } 15 | ---- 16 | 17 | The runtime will recognize that `activemq-spring` is available and will auto configure the `jmsConnectionFactory` in the Spring application context. 18 | The 19 | `org.springframework.boot.autoconfigure.jms.activemq.ActiveMQConnectionFactoryConfiguration` class is what is actually configuring the `jmsConnectionFactory` bean. 20 | If the default factory settings are not sufficient the factory may be configured with any of the properties defined in the 21 | `org.springframework.boot.autoconfigure.jms.activemq.ActiveMQProperties` 22 | class by defining corresponding properties in `application.yml` with corresponding property names defined under `spring.activmeq` as shown below. 23 | 24 | [source,groovy] 25 | ---- 26 | spring: 27 | activemq: 28 | brokerUrl: vm://localhost 29 | pool: 30 | enabled: true 31 | jms: 32 | cache: 33 | enabled: false 34 | ---- 35 | 36 | Note for those who want to use connection pooling: when `spring.activemq.pool.enabled` is true then SpringBoot will create a `pooledJmsConnectionFactory` bean rather than `jmsConnectionFactory`. 37 | You can add the following line in `resources.groovy` to use a spring bean alias in order to get around this. 38 | 39 | [source,groovy] 40 | ---- 41 | beans = { 42 | 43 | // ... 44 | springConfig.addAlias('jmsConnectionFactory', 'pooledJmsConnectionFactory') 45 | } 46 | ---- 47 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugin/jms/listener/ListenerConfigTests.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.jms.listener 2 | 3 | import grails.spring.BeanBuilder 4 | 5 | class ListenerConfigTests extends GroovyTestCase { 6 | 7 | def newListenerConfig(properties) { 8 | new ListenerConfig(properties) 9 | } 10 | 11 | def newListenerConfig() { 12 | newListenerConfig([:]) 13 | } 14 | 15 | void testGetBeanPrefix() { 16 | assertEquals("personOnMessage", newListenerConfig( 17 | serviceBeanName: "personService", 18 | listenerMethodName: "onMessage").beanPrefix 19 | ) 20 | assertEquals("person", newListenerConfig( 21 | serviceListener: true, 22 | serviceBeanName: "personService", 23 | listenerMethodName: "onMessage").beanPrefix 24 | ) 25 | } 26 | 27 | void testGetDestinationName() { 28 | def mockGrailsApplication = new ConfigObject() 29 | mockGrailsApplication.metadata["info.app.name"] = "app" 30 | 31 | def lc1 = newListenerConfig( 32 | grailsApplication: mockGrailsApplication, 33 | serviceBeanName: "personService", 34 | serviceListener: true 35 | ) 36 | assertEquals("app.person", lc1.destinationName) 37 | 38 | def lc2 = newListenerConfig( 39 | serviceBeanName: "personService", 40 | serviceListener: false, 41 | listenerMethodName: "doSomething", 42 | topic: true 43 | ) 44 | assertEquals("doSomething", lc2.destinationName) 45 | 46 | def lc3 = newListenerConfig( 47 | grailsApplication: mockGrailsApplication, 48 | serviceBeanName: "personService", 49 | serviceListener: false, 50 | listenerMethodName: "doSomething", 51 | topic: false 52 | ) 53 | assertEquals("app.person.doSomething", lc3.destinationName) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/docs/asciidoc/receivingMessages/usingOtherContainersOrAdapters.adoc: -------------------------------------------------------------------------------- 1 | Here is an example of using a container and adapter other than standard. 2 | 3 | === Example 4 | 5 | ==== resources.groovy 6 | 7 | [source,groovy] 8 | ---- 9 | import org.apache.activemq.ActiveMQConnectionFactory 10 | import org.springframework.jms.connection.SingleConnectionFactory 11 | 12 | beans = { 13 | // used by the standard template by convention 14 | jmsConnectionFactory(SingleConnectionFactory) { 15 | targetConnectionFactory = { ActiveMQConnectionFactory cf -> 16 | brokerURL = 'vm://localhost' 17 | } 18 | } 19 | 20 | otherJmsConnectionFactory(SingleConnectionFactory) { 21 | targetConnectionFactory = { ActiveMQConnectionFactory cf -> 22 | brokerURL = // ... something else 23 | } 24 | } 25 | } 26 | ---- 27 | 28 | ==== Config.groovy 29 | 30 | [source,groovy] 31 | ---- 32 | jms { 33 | containers { 34 | other { 35 | meta { 36 | parentBean = 'standardJmsListenerContainer' 37 | } 38 | concurrentConsumers = 5 39 | connectionFactoryBean = "otherJmsConnectionFactory" 40 | } 41 | } 42 | adapters { 43 | other { 44 | meta { 45 | parentBean = 'standardJmsListenerAdapter' 46 | } 47 | messageConverter = null // do no message conversion 48 | } 49 | } 50 | } 51 | ---- 52 | 53 | ==== Sending messages 54 | 55 | [source,groovy] 56 | ---- 57 | class ListeningService { 58 | static exposes = ["jms"] 59 | static adapter = "other" 60 | static container = "other" 61 | 62 | def onMessage(msg) { 63 | // handle message 64 | } 65 | } 66 | ---- 67 | 68 | [source,groovy] 69 | ---- 70 | import grails.plugin.jms.* 71 | 72 | class ListeningService { 73 | static exposes = ["jms"] 74 | 75 | @Queue(adapter = "other", container = "other") 76 | def receive(msg) { 77 | // handle message 78 | } 79 | } 80 | ---- 81 | -------------------------------------------------------------------------------- /src/docs/asciidoc/examples.adoc: -------------------------------------------------------------------------------- 1 | The following are some simple examples to give you a feel for the plugin. 2 | 3 | === Service Queue Listeners 4 | 5 | [source,groovy] 6 | ---- 7 | class ListeningService { 8 | 9 | static exposes = ['jms'] 10 | 11 | def onMessage(message) { 12 | assert message == 1 13 | } 14 | } 15 | ---- 16 | 17 | [source,groovy] 18 | ---- 19 | class SomeController { 20 | 21 | def jmsService 22 | 23 | def someAction() { 24 | jmsService.send(service: 'listening', 1) 25 | } 26 | } 27 | ---- 28 | 29 | === Service Method Queue Listeners 30 | 31 | [source,groovy] 32 | ---- 33 | import grails.plugin.jms.Queue 34 | 35 | class ListeningService { 36 | 37 | static exposes = ['jms'] 38 | 39 | @Queue 40 | def receive(message) { 41 | assert message == 1 42 | } 43 | } 44 | ---- 45 | 46 | [source,groovy] 47 | ---- 48 | class SomeController { 49 | 50 | def jmsService 51 | 52 | def someAction() { 53 | jmsService.send(service: 'listening', method: 'receive', 1) 54 | } 55 | } 56 | ---- 57 | 58 | === Topic Listeners 59 | 60 | [source,groovy] 61 | ---- 62 | import grails.plugin.jms.Subscriber 63 | 64 | class ListeningService { 65 | 66 | static exposes = ['jms'] 67 | 68 | @Subscriber 69 | def newMessages(message) { 70 | assert message == 1 71 | } 72 | } 73 | ---- 74 | 75 | [source,groovy] 76 | ---- 77 | class SomeController { 78 | 79 | def jmsService 80 | 81 | def someAction() { 82 | jmsService.send(topic: 'newMessages', 1) 83 | } 84 | } 85 | ---- 86 | 87 | === Post Processing Messages 88 | 89 | [source,groovy] 90 | ---- 91 | import javax.jms.Message 92 | 93 | class SomeController { 94 | 95 | def jmsService 96 | 97 | def someAction() { 98 | jmsService.send(service: 'initial', 1) { Message msg -> 99 | msg.JMSReplyTo = createDestination(service: 'reply') 100 | msg 101 | } 102 | } 103 | } 104 | ---- 105 | -------------------------------------------------------------------------------- /.github/workflows/release_docs_only.yml: -------------------------------------------------------------------------------- 1 | name: Rebuild documentation 2 | on: workflow_dispatch 3 | jobs: 4 | release-docs: 5 | runs-on: ubuntu-latest 6 | env: 7 | GIT_USER_NAME: ${{ secrets.GIT_USER_NAME }} 8 | GIT_USER_EMAIL: ${{ secrets.GIT_USER_EMAIL }} 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v2 12 | with: 13 | token: ${{ secrets.GH_TOKEN }} 14 | - uses: gradle/wrapper-validation-action@v1 15 | - name: Set up JDK 16 | uses: actions/setup-java@v1 17 | with: 18 | java-version: 8 19 | - name: Get latest release version number 20 | id: get_version 21 | uses: battila7/get-version-action@v2 22 | - name: Build documentation 23 | env: 24 | RELEASE_VERSION: ${{ steps.get_version.outputs.version-without-v }} 25 | run: | 26 | ./gradlew asciidoctor -Pversion="${RELEASE_VERSION}" 27 | - name: Export Gradle Properties 28 | uses: micronaut-projects/github-actions/export-gradle-properties@master 29 | - name: Publish to Github Pages 30 | if: success() 31 | uses: micronaut-projects/github-pages-deploy-action@master 32 | env: 33 | BETA: ${{ steps.get_version.outputs.isPrerelase }} 34 | TARGET_REPOSITORY: ${{ github.repository }} 35 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 36 | BRANCH: gh-pages 37 | FOLDER: build/asciidoc 38 | DOC_FOLDER: latest 39 | COMMIT_EMAIL: ${{ secrets.GIT_USER_EMAIL }} 40 | COMMIT_NAME: ${{ secrets.GIT_USER_NAME }} 41 | VERSION: ${{ steps.get_version.outputs.version-without-v }} 42 | - name: Run post-release 43 | if: success() 44 | uses: micronaut-projects/github-actions/post-release@master 45 | with: 46 | token: ${{ secrets.GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/listener/adapter/PersistenceContextAwareListenerAdapter.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Grails Plugin Collective 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package grails.plugin.jms.listener.adapter 17 | 18 | import javax.jms.JMSException 19 | import javax.jms.Message 20 | import javax.jms.Session 21 | 22 | class PersistenceContextAwareListenerAdapter extends LoggingListenerAdapter { 23 | 24 | def persistenceInterceptor 25 | 26 | // Needed to workaround groovy bug with call to super in LoggingListenerAdapter 27 | void onMessage(Message message) { 28 | super.onMessage(message) 29 | } 30 | 31 | // Needed to workaround groovy bug with call to super in LoggingListenerAdapter 32 | void onMessage(Message message, Session session) { 33 | super.onMessage(message, session) 34 | } 35 | 36 | protected invokeListenerMethod(String methodName, Object[] arguments) throws JMSException { 37 | try { 38 | if (persistenceInterceptor) { 39 | log.debug("opening persistence context for listener $methodName of $delegate") 40 | persistenceInterceptor.init() 41 | } 42 | else { 43 | log.debug("no persistence interceptor for listener $methodName of $delegate") 44 | } 45 | super.invokeListenerMethod(methodName, *arguments) 46 | } 47 | finally { 48 | if (persistenceInterceptor) { 49 | log.debug("destroying persistence context for listener $methodName of $delegate") 50 | persistenceInterceptor.flush() 51 | persistenceInterceptor.destroy() 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugin/jms/JmsServiceConfSpec.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.jms 2 | 3 | import grails.testing.services.ServiceUnitTest 4 | import spock.lang.Specification 5 | import spock.lang.Unroll 6 | 7 | class JmsServiceConfSpec extends Specification implements ServiceUnitTest { 8 | 9 | @Unroll("the calculated receiver timeout should follow rule [#rule]") 10 | def "receiveTimeout"() { 11 | 12 | given: "a configuration" 13 | config.jms = [receiveTimeout: configReceiveTimeout] 14 | 15 | and: "a given jmsTemplate that has a timeout" 16 | def jmsTemplate = [receiveTimeout: aJmsTemplateTimeout] 17 | 18 | when: "ask which timeout we should use" 19 | long timeout = service.calculatedReceiverTimeout(aCallReceiveTimeout, jmsTemplate) 20 | 21 | then: 22 | timeout == expected 23 | 24 | where: 25 | rule | aCallReceiveTimeout | configReceiveTimeout | aJmsTemplateTimeout | expected 26 | "explicit timeout above all" | 5 | 3 | 7 | 5 27 | "template above conf if not 0" | null | 3 | 7 | 7 28 | "conf above template if template is 0" | null | 3 | 0 | 3 29 | "use default if conf and template are 0" | null | 0 | 0 | JmsService.DEFAULT_RECEIVER_TIMEOUT_MILLIS 30 | "use default if none provided" | null | null | 0 | JmsService.DEFAULT_RECEIVER_TIMEOUT_MILLIS 31 | } 32 | 33 | @Unroll("the JmsService should #action if the jms config is set to [#disabledConfig]") 34 | def "enable-disable"() { 35 | when: 36 | config.jms = [disabled: disabledConfig] 37 | 38 | then: 39 | service.disabled == expected 40 | 41 | where: 42 | action | disabledConfig | expected 43 | "default to enabled"| null | false 44 | "be disabled" | true | true 45 | "be enabled" | false | false 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugin/jms/JmsServiceAsyncExecutorSpec.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.jms 2 | 3 | 4 | import java.util.concurrent.Executors 5 | 6 | import spock.lang.* 7 | 8 | class JmsServiceAsyncExecutorSpec extends Specification { 9 | 10 | def jmsService = new JmsService() 11 | 12 | @AutoCleanup("shutdown") 13 | def executor = Executors.newCachedThreadPool() 14 | 15 | @AutoCleanup("shutdown") 16 | def executor2 = Executors.newCachedThreadPool() 17 | 18 | def "we can set an Async Receiver Executor"() { 19 | when: "we set the executor in the service" 20 | jmsService.asyncReceiverExecutor = executor 21 | 22 | and: "we 'destroy' the service bean" 23 | jmsService.destroy() 24 | 25 | then: "the executor should receive a shutdown request" 26 | jmsService.asyncReceiverExecutor == executor 27 | executor.shutdown 28 | } 29 | 30 | def "setting a second executor will shutdown the first one"() { 31 | when: "we assign one executor" 32 | jmsService.asyncReceiverExecutor = executor 33 | 34 | and: "after that we set another" 35 | jmsService.asyncReceiverExecutor = executor2 36 | 37 | then: "the first executor should receive a shutdown request" 38 | jmsService.asyncReceiverExecutor == executor2 39 | executor.shutdown 40 | } 41 | 42 | def "we can disable auto-shutdown of the executor on destruction"() { 43 | given: "an executor that is set" 44 | jmsService.asyncReceiverExecutor = executor 45 | 46 | and: "the disablement of the shutdown flag" 47 | jmsService.asyncReceiverExecutorShutdown = false 48 | 49 | when: "we destroy the service" 50 | jmsService.destroy() 51 | 52 | then: "the executor shouldn't receive a shutdown request" 53 | jmsService.asyncReceiverExecutor == executor 54 | !executor.shutdown 55 | } 56 | 57 | def "we get a default executor if none is set"() { 58 | given: "a service" 59 | assert jmsService 60 | 61 | when:"we get the default executor" 62 | def _executor = jmsService.asyncReceiverExecutor 63 | 64 | and: "we destroy the service" 65 | jmsService.destroy() 66 | 67 | then: "the executor shouldn't be null and should have receive a shutdown request" 68 | _executor 69 | _executor.shutdown 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/docs/asciidoc/sendingMessages.adoc: -------------------------------------------------------------------------------- 1 | This plugin adds a service called `jmsService` to your application that can be used to send JMS messages. 2 | 3 | 4 | == The send(destination, message, jmsTemplateName, postProcessor) method. 5 | 6 | *destination* 7 | 8 | An instance of `javax.jms.Destination`, `javax.jms.Topic` , a `String` or a `Map` . 9 | 10 | A `String` destination argument will be interpreted as the name of a destination _queue_ . 11 | 12 | A `Map` destination argument can be used in the following ways: 13 | 14 | [source,groovy] 15 | ---- 16 | jmsService.send(queue: "aQueue", msg, "standard", null) // send to a literal queue 17 | 18 | jmsService.send(topic: "aTopic", msg, "standard", null) // send to a literal topic 19 | 20 | jmsService.send(service: "person", msg, "standard", null) // send to the queue '«appname».person' 21 | 22 | jmsService.send(service: "person", method: "doIt", msg, "standard", null) // send to the queue '«appname».person.doIt' 23 | 24 | jmsService.send(app: "remote", service: "person", method: "doIt", msg, "standard", null) // send to the queue 'remote.person.doIt' 25 | ---- 26 | 27 | The app/service/method convention makes a lot more sense if you read the section below on service method listener queue subscribers. 28 | 29 | *message* 30 | 31 | This is the message payload. 32 | By default this can be any Java/Groovy object or a javax.jms.Message. 33 | How it gets converted into a message is handled by the underlying jms template's message converter. 34 | 35 | *jmsTemplateName* 36 | 37 | The name of the template that should be used to send the message. 38 | If this value is `null` , the standard template will be used (called "standard"). 39 | 40 | *postProcessor* 41 | 42 | An optional closure that can be used to "post process" the message after it has been converted into a message but before it has been sent. 43 | This closure acts as the implementation of the http://static.springframework.org/spring/docs/2.0.x/api/org/springframework/jms/core/MessagePostProcessor.html#postProcessMessage(javax.jms.Message)[postProcessMessage()] method of the http://static.springframework.org/spring/docs/2.0.x/api/org/springframework/jms/core/MessagePostProcessor.html[MessagePostProcessor] class. 44 | 45 | 46 | === send() method variants 47 | 48 | There are variations of the send() method for convenience... 49 | 50 | [source,java] 51 | ---- 52 | jmsService.send(destination, message) // use the standard template and no post processor 53 | jmsService.send(destination, message, postProcessor) // use the standard template 54 | ---- 55 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/listener/adapter/LoggingListenerAdapter.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Grails Plugin Collective 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package grails.plugin.jms.listener.adapter 17 | 18 | import javax.jms.Message 19 | import javax.jms.Session 20 | 21 | import org.apache.commons.lang.StringUtils 22 | import org.apache.commons.logging.LogFactory 23 | import org.springframework.beans.factory.InitializingBean 24 | import org.springframework.jms.listener.adapter.MessageListenerAdapter 25 | 26 | class LoggingListenerAdapter extends MessageListenerAdapter implements InitializingBean { 27 | 28 | protected log 29 | 30 | void afterPropertiesSet() { 31 | log = createLog() 32 | } 33 | 34 | void onMessage(Message message) { 35 | if (log.debugEnabled) { 36 | log.debug("receiving message $message.JMSMessageID ($message.JMSDestination)") 37 | } 38 | super.onMessage(message) 39 | if (log.debugEnabled) { 40 | log.debug("received message $message.JMSMessageID ($message.JMSDestination)") 41 | } 42 | } 43 | 44 | void onMessage(Message message, Session session) { 45 | if (log.debugEnabled) { 46 | log.debug("receiving message (in session) $message.JMSMessageID ($message.JMSDestination)") 47 | } 48 | try { 49 | super.onMessage(message, session) 50 | if (log.debugEnabled) { 51 | log.debug("received message (in session) $message.JMSMessageID ($message.JMSDestination)") 52 | } 53 | } 54 | catch (Throwable e) { 55 | handleListenerException(e) 56 | throw e 57 | } 58 | } 59 | 60 | protected void handleListenerException(Throwable ex) { 61 | if (log.errorEnabled) { 62 | log.error("Exception raised in message listener", ex) 63 | } 64 | } 65 | 66 | protected createLog() { 67 | LogFactory.getLog("${getClass().name}.${StringUtils.uncapitalize(delegate.getClass().name - 'Service')}.${defaultListenerMethod}".toString()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugin/jms/JmsGrailsPluginConfSpec.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.jms 2 | 3 | import grails.config.Config 4 | import grails.core.GrailsApplication 5 | import org.grails.config.NavigableMapConfig 6 | import org.grails.config.PropertySourcesConfig 7 | import spock.lang.Specification 8 | import spock.lang.Unroll 9 | 10 | class JmsGrailsPluginConfSpec extends Specification { 11 | 12 | def jmsGrailsPlugin = new JmsGrailsPlugin() 13 | 14 | def "useDefaults"() { 15 | 16 | when: "no local JMS configuration" 17 | jmsGrailsPlugin.grailsApplication = Mock(GrailsApplication) 18 | jmsGrailsPlugin.grailsApplication.getConfig() >> Mock(Config) 19 | 20 | then: 21 | def jmsConfig = jmsGrailsPlugin.getJmsConfigurationWithDefaults() 22 | 23 | expect: 24 | jmsConfig == jmsGrailsPlugin.getDefaultConfig() 25 | 26 | } 27 | 28 | @Unroll("overriding default configuration should follow the rule [#rule]") 29 | def "overrideDefaults"() { 30 | given: "create a config" 31 | Config config = createConfig(configContent) 32 | 33 | when: 34 | jmsGrailsPlugin.grailsApplication = Mock(GrailsApplication) 35 | jmsGrailsPlugin.grailsApplication.getConfig() >> config 36 | NavigableMapConfig jmsConfig = toNavigableMapConfig(jmsGrailsPlugin.getJmsConfigurationWithDefaults()) 37 | 38 | then: 39 | jmsConfig.navigate( (String[])configPath.toArray()) == expected 40 | 41 | where: 42 | rule | configContent | configPath | expected 43 | "override simple property" | [jms: [disabled: true]] | ['disabled'] | true 44 | "adding new poperty" | [jms: [newProp: 'test']] | ['newProp'] | 'test' 45 | "overrriding deep default property" | [jms: [templates: [standard: [connectionFactoryBean: "pooledJmsConnectionFactory"]]]] | ['templates', 'standard', 'connectionFactoryBean'] | 'pooledJmsConnectionFactory' 46 | "overriding only one deep default property's map value" | [jms: [templates: [standard: [connectionFactoryBean: "pooledJmsConnectionFactory"]]]] | ['templates', 'standard', 'messageConverterBean'] | 'standardJmsMessageConverter' 47 | } 48 | 49 | 50 | 51 | private NavigableMapConfig toNavigableMapConfig(def config) { 52 | if(config instanceof NavigableMapConfig) { 53 | return (NavigableMapConfig)config 54 | } 55 | else { 56 | PropertySourcesConfig navConfig = new PropertySourcesConfig() 57 | navConfig.putAll(config) 58 | return navConfig 59 | } 60 | } 61 | 62 | private Config createConfig(Map configContent) { 63 | return new PropertySourcesConfig(configContent) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugin/jms/listener/ServiceInspectorSpec.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.jms.listener 2 | 3 | import org.grails.testing.GrailsUnitTest 4 | import spock.lang.Specification 5 | import spock.lang.Unroll 6 | 7 | class ServiceInspectorSpec extends Specification implements GrailsUnitTest { 8 | 9 | ServiceInspector serviceInspector = new ServiceInspector() 10 | 11 | def 'know if a service is exposes through Jms'() { 12 | expect: 13 | serviceInspector.exposesJms(DoesExposeJms) 14 | serviceInspector.exposesJms(DoesExposesJms) 15 | !serviceInspector.exposesJms(DoesntExposeJms) 16 | } 17 | 18 | def 'know if a Service is a Singleton'() { 19 | expect: 20 | serviceInspector.isSingleton(ImplicitSingleton) 21 | serviceInspector.isSingleton(ExplicitSingleton) 22 | !serviceInspector.isSingleton(NonSingleton) 23 | } 24 | 25 | def 'know if a Service has a Listener Method'() { 26 | expect: 27 | serviceInspector.hasServiceListenerMethod(HasServiceListenerMethod) 28 | !serviceInspector.hasServiceListenerMethod(HasNoServiceListener) 29 | } 30 | 31 | @Unroll("key [#key] resolves to expected value [#expected] with conf [#configuration]") 32 | def 'able to resolve destination names through configuration'() { 33 | given: 34 | config.merge(configuration) 35 | 36 | when: 37 | String obtained = serviceInspector.resolveDestinationName(key, grailsApplication) 38 | 39 | then: 40 | expected == obtained 41 | 42 | where: 43 | key | expected | configuration 44 | 'baseCase' | 'baseCase' | [:] 45 | '$queueKey' | 'my.service.queue' | ['jms.destinations.queueKey':'my.service.queue'] 46 | '$a.queue.key' | 'my.service.queue' | ['jms.destinations.a.queue.key':'my.service.queue'] 47 | '$.a.queue.key.' | 'my.service.queue' | ['jms.destinations.a.queue.key':'my.service.queue'] 48 | } 49 | 50 | def 'fail if we are unable to resolve destination names'() { 51 | given: 52 | config.merge(['just.another.entry':'something']) 53 | 54 | when: 55 | serviceInspector.resolveDestinationName('$no.match', grailsApplication) 56 | 57 | then: 58 | thrown(IllegalArgumentException) 59 | } 60 | 61 | def getApplicationContext(String configuration){ 62 | def applicationContext = new ConfigObject() 63 | applicationContext.config = new ConfigSlurper().parse(configuration) 64 | applicationContext 65 | } 66 | 67 | } 68 | 69 | class DoesExposesJms { 70 | static exposes = ["blah", "jms"] 71 | } 72 | class DoesExposeJms { 73 | static expose = ["blah", "jms"] 74 | } 75 | class DoesntExposeJms {} 76 | 77 | class ImplicitSingleton {} 78 | 79 | class ExplicitSingleton { 80 | static scope = "singleton" 81 | } 82 | class NonSingleton { 83 | static scope = "session" 84 | } 85 | class DefaultServiceListenerName {} 86 | 87 | class HasServiceListenerMethod { 88 | def onMessage(msg) {} 89 | } 90 | class HasNoServiceListener {} 91 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/bean/MapBasedBeanDefinitionBuilder.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Grails Plugin Collective 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package grails.plugin.jms.bean 17 | 18 | class MapBasedBeanDefinitionBuilder { 19 | 20 | private static final String BEAN_QUALIFIER = 'Bean' 21 | 22 | private name 23 | private definition 24 | 25 | MapBasedBeanDefinitionBuilder(name, Map definition) { 26 | this.name = name 27 | this.definition = definition 28 | } 29 | 30 | def getName() { 31 | name 32 | } 33 | 34 | def getClazz() { 35 | definition.clazz 36 | } 37 | 38 | def getMeta() { 39 | definition.meta 40 | } 41 | 42 | def getProperties() { 43 | def properties = definition.clone() 44 | properties.remove('clazz') 45 | properties.remove('meta') 46 | properties 47 | } 48 | 49 | def build(beanBuilder) { 50 | beanBuilder.with { 51 | "${getName()}"(clazz) { metaBean -> 52 | def bean = delegate 53 | 54 | this.meta.each { k, v -> 55 | this.set(k, v, metaBean, beanBuilder) 56 | } 57 | this.properties.each { k, v -> 58 | this.set(k, v, bean, beanBuilder) 59 | } 60 | } 61 | } 62 | } 63 | 64 | def removeFrom(context) { 65 | def beanName = getName() 66 | context.containsBean(beanName) && context.removeBeanDefinition(beanName) 67 | } 68 | 69 | /** 70 | * Will bind a property to the given recipient. If such property qualifies as a 71 | * Spring Bean, it's name has the {@link MapBasedBeanDefinitionBuilder#BEAN_QUALIFIER} as suffix, 72 | * it will assign the bean named as {@code value} if such {@code value} is not {@code null} (i.e. {@code value ? ref(value) : null } ). 73 | * If such name doesn't qualify as a bean the given value will be assigned directly to the attribute named as {@code name}. 74 | * @param name Name of the attribute, if suffixed by {@link MapBasedBeanDefinitionBuilder#BEAN_QUALIFIER} it will reference a Spring Bean. 75 | * @param value direct value or name of the bean in the Application Context 76 | * @param recipient 77 | * @param beanBuilder 78 | * @return 79 | */ 80 | protected set(name, value, recipient, beanBuilder) { 81 | if (name.endsWith(BEAN_QUALIFIER)) { 82 | recipient."${name.substring(0, name.size() - BEAN_QUALIFIER.size())}" = value ? beanBuilder.ref(value) : null 83 | } 84 | else { 85 | recipient."$name" = value 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /src/docs/asciidoc/index.adoc: -------------------------------------------------------------------------------- 1 | = JMS Plugin 2 | :version: x.y.z 3 | :source-highlighter: coderay 4 | :imagesdir: ./images 5 | 6 | [[introduction]] 7 | == Introduction 8 | 9 | include::introduction.adoc[] 10 | 11 | [[installation]] 12 | == Installation 13 | include::installation.adoc[] 14 | 15 | [[examples]] 16 | == Examples 17 | include::examples.adoc[] 18 | 19 | [[springJms]] 20 | == Spring JMS 21 | 22 | include::introduction/springJms.adoc[] 23 | 24 | [[jmsProvider]] 25 | == Plugging In A JMS Provider 26 | 27 | include::jmsProvider.adoc[] 28 | 29 | [[activeMqExample]] 30 | == ActiveMQ Example 31 | 32 | include::jmsProvider/activeMqExample.adoc[] 33 | 34 | [[configuration]] 35 | == The Configuration Mechanism 36 | 37 | include::configuration.adoc[] 38 | 39 | [[changingDefaults]] 40 | == Changing Defaults 41 | 42 | include::configuration/changingDefaults.adoc[] 43 | 44 | [[syntaxNotes]] 45 | == Syntax Notes 46 | 47 | include::configuration/syntaxNotes.adoc[] 48 | 49 | [[sendingMessages]] 50 | == Sending Messages 51 | 52 | include::sendingMessages.adoc[] 53 | 54 | [[postProcessing]] 55 | == Post Processing Messages 56 | 57 | include::sendingMessages/postProcessing.adoc[] 58 | 59 | [[usingOtherTemplates]] 60 | == Using Other Templates 61 | 62 | include::sendingMessages/usingOtherTemplates.adoc[] 63 | 64 | [[receivingMessages]] 65 | == Receiving Messages 66 | 67 | include::receivingMessages.adoc[] 68 | 69 | [[serviceListeners]] 70 | == Service Listeners 71 | 72 | include::receivingMessages/serviceListeners.adoc[] 73 | 74 | [[serviceMethodListeners]] 75 | == Service Method Listeners 76 | 77 | include::receivingMessages/serviceMethodListeners.adoc[] 78 | 79 | [[listenerReturnValues]] 80 | == Listener Return Values 81 | 82 | include::receivingMessages/listenerReturnValues.adoc[] 83 | 84 | [[usingOtherContainersOrAdapters]] 85 | == Using Other Containers Or Adapters 86 | 87 | include::receivingMessages/usingOtherContainersOrAdapters.adoc[] 88 | 89 | [[receivingMessagesWithSelectors]] 90 | == Receiving Messages With Selectors 91 | 92 | include::receivingMessagesWithSelectors.adoc[] 93 | 94 | [[receivingMethodsAddedToControllersAndServices]] 95 | == Receiving Methods Added To Controllers And Services 96 | 97 | include::receivingMessagesWithSelectors/receivingMethodsAddedToControllersAndServices.adoc[] 98 | 99 | [[browsingMessagesInQueue]] 100 | == Browsing Messages In A Queue 101 | 102 | include::browsingMessagesInQueue.adoc[] 103 | 104 | [[messageConversion]] 105 | == Message Conversion 106 | 107 | include::messageConversion.adoc[] 108 | 109 | [[logging]] 110 | == Logging 111 | 112 | include::logging.adoc[] 113 | 114 | [[disablingAndReloading]] 115 | == Disabling And Reloading 116 | 117 | include::disablingAndReloading.adoc[] 118 | 119 | [[reference]] 120 | == Reference 121 | 122 | link:./api[API Documentation] 123 | 124 | [[ref-service]] 125 | == Service 126 | 127 | [[ref-service-sendJMSMessage]] 128 | === sendJMSMessage 129 | 130 | include::ref/Service/sendJMSMessage.adoc[] 131 | 132 | [[ref-service-sendTopicJMSMessage]] 133 | === sendTopicJMSMessage 134 | 135 | include::ref/Service/sendTopicJMSMessage.adoc[] 136 | 137 | [[ref-service-receiveSelectedAsyncJMSMessage]] 138 | === receiveSelectedAsyncJMSMessage 139 | 140 | include::ref/Service/receiveSelectedAsyncJMSMessage.adoc[] 141 | 142 | [[ref-service-receiveSelectedJMSMessage]] 143 | === receiveSelectedJMSMessage 144 | 145 | include::ref/Service/receiveSelectedJMSMessage.adoc[] 146 | 147 | [[ref-service-sendPubSubJMSMessage]] 148 | === sendPubSubJMSMessage 149 | 150 | include::ref/Service/sendPubSubJMSMessage.adoc[] 151 | 152 | [[ref-service-sendQueueJMSMessage]] 153 | === sendQueueJMSMessage 154 | 155 | include::ref/Service/sendQueueJMSMessage.adoc[] 156 | 157 | -------------------------------------------------------------------------------- /src/docs/asciidoc/receivingMessages/serviceMethodListeners.adoc: -------------------------------------------------------------------------------- 1 | == Service Method Listeners 2 | 3 | Another avenue is to expose specific methods as message listeners via annotations. 4 | This looks like… 5 | 6 | [source,java] 7 | ---- 8 | import grails.plugin.jms.* 9 | 10 | class PersonService { 11 | static exposes = ["jms"] 12 | 13 | @Queue 14 | def addPerson(msg) { 15 | } 16 | 17 | @Subscriber 18 | def somethingHappened(msg) { 19 | } 20 | } 21 | ---- 22 | 23 | The above configuration binds the `personService.addPerson()` method to a queue named `«app name».person.addPerson` and binds the method `personService.somethingHappened()` as a listener to the topic named `somethingHappened` . 24 | 25 | Note that you still need to expose the class via `static exposes = \["jms"]` . 26 | 27 | 28 | === @Queue Configuration 29 | 30 | The following configuration parameters can be set as annotation parameters… 31 | 32 | [format="csv",options="header"] 33 | |== 34 | 35 | Property Name,Type,Default,Description *name*,String,«app name».«service name».«method name»,The destination name for the queue *selector*,String,null,The message selector to apply (See the “Message Selector” section of http://java.sun.com/j2ee/1.4/docs/api/javax/jms/Message.html) *adapter*,String,"standard",The adapter to use for this listener *container*,String,"standard",The container to use for this listener |== 36 | 37 | Example… 38 | 39 | [source,java] 40 | ---- 41 | import grails.plugin.jms.* 42 | 43 | class PersonService { 44 | static exposes = ["jms"] 45 | 46 | @Queue( 47 | name = "myQueue", 48 | selector = "name IS NOT NULL" 49 | ) 50 | def addPerson(msg) { 51 | } 52 | } 53 | ---- 54 | 55 | === @Subscriber Configuration 56 | 57 | The following configuration parameters can be set as annotation parameters… 58 | 59 | [format="csv",options="header"] 60 | |== 61 | 62 | Property Name,Type,Default,Description *topic*,String,«method name»,The name of the topic to subscribe to *selector*,String,null,The message selector to apply (See the “Message Selector” section of [http://java.sun.com/j2ee/1.4/docs/api/javax/jms/Message.html]) *adapter*,String,"standard",The adapter to use for this listener *container*,String,"standard",The container to use for this listener |== 63 | 64 | Example… 65 | 66 | [source,java] 67 | ---- 68 | import grails.plugin.jms.* 69 | 70 | class PersonService { 71 | static exposes = ["jms"] 72 | 73 | @Subscriber(topic = "aTopic") 74 | def somethingHappened(msg) { 75 | } 76 | } 77 | ---- 78 | 79 | === Defining the Queue names and Subscriber topics through configuration. 80 | 81 | You can specify the names of the given _destinations_ , _queues_ and _topics_ , described through the Queue and Subscriber annotations by prefixing the _key_ with a Dollar sign ( `$` ). 82 | The key needs to be available through the `Config.groovy` file in the `jms.destinations` space, if its not available an *error* will be thrown. 83 | 84 | Example… 85 | 86 | PersonService.groovy 87 | 88 | [source,groovy] 89 | ---- 90 | import grails.plugin.jms.* 91 | 92 | class PersonService { 93 | static exposes = ["jms"] 94 | 95 | @Subscriber(topic = '$topic.key.in.config') 96 | def somethingHappened(msg) { 97 | } 98 | 99 | @Queue(name = '$queue.key.in.config') 100 | def someWorkToDo(msg) { 101 | } 102 | } 103 | ---- 104 | 105 | Config.groovy 106 | 107 | [source,groovy] 108 | ---- 109 | jms { 110 | destinations { 111 | //Name of the topic in the JMS server will be person.somethingHappened 112 | topic.key.in.config = 'person.somethingHappened' 113 | 114 | //Name of the queue in the JMS server will be person.sendSomeWork 115 | queue.key.in.config = 'person.sendSomeWork' 116 | } 117 | } 118 | ---- 119 | -------------------------------------------------------------------------------- /src/docs/asciidoc/receivingMessagesWithSelectors/receivingMethodsAddedToControllersAndServices.adoc: -------------------------------------------------------------------------------- 1 | NOTE: The methods described below are not supported in the current 2.0.0 milestone but will be added soon. 2 | 3 | The plugin will inject the following methods to `Controllers` and `Services`. 4 | 5 | 6 | === Synchronous calls. 7 | 8 | [source,groovy] 9 | ---- 10 | // Expect/Receive a message with a *selector* on a literal queue waiting up to the given *timeout*. 11 | // Will return the converted message or null if the message was not available. 12 | def msg =receiveSelectedJMSMessage(queue: "aQueue", selector, timeout, "standard") 13 | 14 | 15 | // Expect/Receive a message with a *selector* on a literal topic waiting up to the given *timeout*. 16 | // Will return the converted message or null if the message was not available. 17 | def msg =receiveSelectedJMSMessage(topic: "aTopic", selector, timeout, "standard") 18 | 19 | 20 | // Expect/Receive a message with a *selector* on the queue '«appname».person' waiting up to the given *timeout*. 21 | // Will return the converted message or null if the message was not available. 22 | def msg =receiveSelectedJMSMessage(service: "person", selector, timeout, "standard") 23 | 24 | // Expect/Receive a message with a *selector* on the queue '«appname».person.doIt' waiting up to the given *timeout*. 25 | // Will return the converted message or null if the message was not available. 26 | def msg =receiveSelectedJMSMessage(service: "person", method: "doIt", selector, timeout, "standard") 27 | 28 | 29 | // Expect/Receive a message with a *selector* on the queue 'remote.person.doIt' waiting up to the given *timeout*. 30 | // Will return the converted message or null if the message was not available. 31 | def msg = receiveSelectedJMSMessage(app: "remote", service: "person", method: "doIt", selector, timeout, "standard") 32 | ---- 33 | 34 | === Asynchronous calls. 35 | 36 | [source,groovy] 37 | ---- 38 | // Expect/Receive a message with a *selector* on a literal queue waiting up to the given *timeout*. 39 | // Will return a java.util.concurrent.Future wrapping the result the task. 40 | def afuture = receiveSelectedAsyncJMSMessage(queue: "aQueue", selector, timeout, "standard") 41 | 42 | 43 | // Expect/Receive a message with a *selector* on a literal topic waiting up to the given *timeout*. 44 | // Will return a java.util.concurrent.Future wrapping the result the task. 45 | def afuture = receiveSelectedAsyncJMSMessage(topic: "aTopic", selector, timeout, "standard") 46 | 47 | 48 | // Expect/Receive a message with a *selector* on the queue '«appname».person' waiting up to the given *timeout*. 49 | // Will return a java.util.concurrent.Future wrapping the result the task. 50 | def afuture = receiveSelectedAsyncJMSMessage(service: "person", selector, timeout, "standard") 51 | 52 | // Expect/Receive a message with a *selector* on the queue '«appname».person.doIt' waiting up to the given *timeout*. 53 | // Will return a java.util.concurrent.Future wrapping the result the task. 54 | def afuture = receiveSelectedAsyncJMSMessage(service: "person", method: "doIt", selector, timeout, "standard") 55 | 56 | 57 | // Expect/Receive a message with a *selector* on the queue 'remote.person.doIt' waiting up to the given *timeout*. 58 | // Will return a java.util.concurrent.Future wrapping the result the task. 59 | def afuture = receiveSelectedAsyncJMSMessage(app: "remote", service: "person", method: "doIt", selector, timeout, "standard") 60 | ---- 61 | 62 | **Note: a afuture.get() will return the *message*. 63 | 64 | === Specifying your own **Executor** for Async. Receivers using *Spring IoC*. 65 | 66 | [source,groovy] 67 | ---- 68 | beans = { 69 | jmsAsyncReceiverExecutor( java.util.concurrent.Executors ) { executors -> 70 | executors.factoryMethod = "newFixedThreadPool" 71 | executors.constructorArgs = << 5 >> 72 | } 73 | } 74 | ---- 75 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/listener/ListenerConfig.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Grails Plugin Collective 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package grails.plugin.jms.listener 17 | 18 | import grails.plugin.jms.bean.JmsListenerAdapterAbstractBeanDefinitionBuilder 19 | import grails.plugin.jms.bean.JmsListenerContainerAbstractBeanDefinitionBuilder 20 | 21 | import org.apache.commons.lang.StringUtils 22 | 23 | class ListenerConfig { 24 | 25 | static final String SERVICE_BEAN_SUFFIX = "Service" 26 | 27 | static final String DEFAULT_CONNECTION_FACTORY_BEAN_NAME = "jmsConnectionFactory" 28 | 29 | def grailsApplication 30 | 31 | boolean topic = false 32 | def listenerMethodName 33 | def messageSelector 34 | def explicitDestinationName 35 | def serviceListener = false 36 | def serviceBeanName 37 | def containerParent 38 | def adapterParent 39 | 40 | def getServiceBeanPrefix() { 41 | serviceBeanName - SERVICE_BEAN_SUFFIX 42 | } 43 | 44 | def getBeanPrefix() { 45 | if (serviceListener) { 46 | serviceBeanPrefix 47 | } 48 | else { 49 | serviceBeanPrefix + StringUtils.capitalize(listenerMethodName) 50 | } 51 | } 52 | 53 | def getListenerAdapterBeanName() { 54 | beanPrefix + "JmsListenerAdapter" 55 | } 56 | 57 | def getListenerContainerBeanName() { 58 | beanPrefix + "JmsListenerContainer" 59 | } 60 | 61 | def getDestinationName() { 62 | if (explicitDestinationName) { 63 | explicitDestinationName 64 | } 65 | else { 66 | if (serviceListener) { 67 | appName + "." + serviceBeanPrefix 68 | } 69 | else if (topic) { 70 | listenerMethodName 71 | } 72 | else { 73 | appName + "." + serviceBeanPrefix + "." + listenerMethodName 74 | } 75 | } 76 | } 77 | 78 | def getAppName() { 79 | grailsApplication.metadata['info.app.name'] 80 | } 81 | 82 | def register(beanBuilder) { 83 | registerListenerAdapter(beanBuilder) 84 | registerListenerContainer(beanBuilder) 85 | } 86 | 87 | def registerListenerAdapter(beanBuilder) { 88 | beanBuilder.with { 89 | "${listenerAdapterBeanName}" { 90 | it.parent = ref(adapterParent + JmsListenerAdapterAbstractBeanDefinitionBuilder.nameSuffix) 91 | it.'abstract' = false 92 | delegate.delegate = ref(serviceBeanName) 93 | defaultListenerMethod = listenerMethodName 94 | } 95 | } 96 | } 97 | 98 | def registerListenerContainer(beanBuilder) { 99 | beanBuilder.with { 100 | "${listenerContainerBeanName}"() { 101 | it.parent = ref(containerParent + JmsListenerContainerAbstractBeanDefinitionBuilder.nameSuffix) 102 | it.'abstract' = false 103 | it.destroyMethod = "destroy" 104 | 105 | destinationName = this.destinationName 106 | 107 | pubSubDomain = topic 108 | if (messageSelector) { 109 | messageSelector = messageSelector 110 | } 111 | 112 | messageListener = ref(listenerAdapterBeanName) 113 | } 114 | } 115 | } 116 | 117 | def removeBeansFromContext(ctx) { 118 | [listenerAdapterBeanName,listenerContainerBeanName].each { 119 | ctx.removeBeanDefinition(it) 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: [ published ] 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | env: 9 | GIT_USER_NAME: ${{ secrets.GIT_USER_NAME }} 10 | GIT_USER_EMAIL: ${{ secrets.GIT_USER_EMAIL }} 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | with: 15 | token: ${{ secrets.GH_TOKEN }} 16 | - uses: gradle/wrapper-validation-action@v1 17 | - name: Set up JDK 18 | uses: actions/setup-java@v1 19 | with: 20 | java-version: 8 21 | - name: Get latest release version number 22 | id: get_version 23 | uses: battila7/get-version-action@v2 24 | - name: Run pre-release 25 | uses: micronaut-projects/github-actions/pre-release@master 26 | with: 27 | token: ${{ secrets.GITHUB_TOKEN }} 28 | - name: Publish to Sonatype OSSRH 29 | env: 30 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 31 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 32 | SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} 33 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} 34 | SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} 35 | SECRING_FILE: ${{ secrets.SECRING_FILE }} 36 | RELEASE_VERSION: ${{ steps.get_version.outputs.version-without-v }} 37 | run: | 38 | echo "${SECRING_FILE}" | base64 -d > "${GITHUB_WORKSPACE}/secring.gpg" 39 | echo "Publishing Artifacts for $RELEASE_VERSION" 40 | (set -x; ./gradlew -Pversion="${RELEASE_VERSION}" -Psigning.secretKeyRingFile="${GITHUB_WORKSPACE}/secring.gpg" publishToSonatype closeAndReleaseSonatypeStagingRepository --no-daemon) 41 | - name: Bump patch version by one 42 | uses: actions-ecosystem/action-bump-semver@v1 43 | id: bump_semver 44 | with: 45 | current_version: ${{steps.get_version.outputs.version-without-v }} 46 | level: patch 47 | - name: Set version in gradle.properties 48 | env: 49 | NEXT_VERSION: ${{ steps.bump_semver.outputs.new_version }} 50 | run: | 51 | echo "Preparing next snapshot" 52 | ./gradlew snapshotVersion -Pversion="${NEXT_VERSION}" 53 | - name: Commit & Push changes 54 | uses: actions-js/push@master 55 | with: 56 | github_token: ${{ secrets.GITHUB_TOKEN }} 57 | author_name: ${{ secrets.GIT_USER_NAME }} 58 | author_email: $${ secrets.GIT_USER_EMAIL }} 59 | message: 'Set version to next SNAPSHOT' 60 | - name: Build documentation 61 | env: 62 | RELEASE_VERSION: ${{ steps.get_version.outputs.version-without-v }} 63 | run: | 64 | ./gradlew asciidoctor -Pversion="${RELEASE_VERSION}" 65 | - name: Export Gradle Properties 66 | uses: micronaut-projects/github-actions/export-gradle-properties@master 67 | - name: Publish to Github Pages 68 | if: success() 69 | uses: micronaut-projects/github-pages-deploy-action@master 70 | env: 71 | BETA: ${{ steps.get_version.outputs.isPrerelase }} 72 | TARGET_REPOSITORY: ${{ github.repository }} 73 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 74 | BRANCH: gh-pages 75 | FOLDER: build/asciidoc 76 | DOC_FOLDER: latest 77 | COMMIT_EMAIL: ${{ secrets.GIT_USER_EMAIL }} 78 | COMMIT_NAME: ${{ secrets.GIT_USER_NAME }} 79 | VERSION: ${{ steps.get_version.outputs.version-without-v }} 80 | - name: Run post-release 81 | if: success() 82 | uses: micronaut-projects/github-actions/post-release@master 83 | with: 84 | token: ${{ secrets.GITHUB_TOKEN }} 85 | -------------------------------------------------------------------------------- /grails-app/conf/application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | grails: 3 | profile: web-plugin 4 | codegen: 5 | defaultPackage: jms 6 | mime: 7 | disable: 8 | accept: 9 | header: 10 | userAgents: 11 | - Gecko 12 | - WebKit 13 | - Presto 14 | - Trident 15 | types: 16 | all: '*/*' 17 | atom: application/atom+xml 18 | css: text/css 19 | csv: text/csv 20 | form: application/x-www-form-urlencoded 21 | html: 22 | - text/html 23 | - application/xhtml+xml 24 | js: text/javascript 25 | json: 26 | - application/json 27 | - text/json 28 | multipartForm: multipart/form-data 29 | rss: application/rss+xml 30 | text: text/plain 31 | hal: 32 | - application/hal+json 33 | - application/hal+xml 34 | xml: 35 | - text/xml 36 | - application/xml 37 | urlmapping: 38 | cache: 39 | maxsize: 1000 40 | controllers: 41 | defaultScope: singleton 42 | converters: 43 | encoding: UTF-8 44 | views: 45 | default: 46 | codec: html 47 | gsp: 48 | encoding: UTF-8 49 | htmlcodec: xml 50 | codecs: 51 | expression: html 52 | scriptlets: html 53 | taglib: none 54 | staticparts: none 55 | info: 56 | app: 57 | name: '@info.app.name@' 58 | version: '@info.app.version@' 59 | grailsVersion: '@info.app.grailsVersion@' 60 | spring: 61 | groovy: 62 | template: 63 | check-template-location: false 64 | hibernate: 65 | cache: 66 | queries: false 67 | dataSource: 68 | pooled: true 69 | jmxExport: true 70 | driverClassName: org.h2.Driver 71 | username: sa 72 | password: 73 | 74 | environments: 75 | development: 76 | dataSource: 77 | dbCreate: create-drop 78 | url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE 79 | test: 80 | dataSource: 81 | dbCreate: update 82 | url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE 83 | production: 84 | dataSource: 85 | dbCreate: update 86 | url: jdbc:h2:prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE 87 | properties: 88 | jmxEnabled: true 89 | initialSize: 5 90 | maxActive: 50 91 | minIdle: 5 92 | maxIdle: 25 93 | maxWait: 10000 94 | maxAge: 600000 95 | timeBetweenEvictionRunsMillis: 5000 96 | minEvictableIdleTimeMillis: 60000 97 | validationQuery: SELECT 1 98 | validationQueryTimeout: 3 99 | validationInterval: 15000 100 | testOnBorrow: true 101 | testWhileIdle: true 102 | testOnReturn: false 103 | jdbcInterceptors: ConnectionState 104 | defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED 105 | jms: 106 | disabled: false 107 | 108 | templates: 109 | transacted: 110 | meta: 111 | parentBean: standardJmsTemplate 112 | sessionTransacted: true 113 | standard: 114 | connectionFactoryBean: jmsConnectionFactory 115 | messageConverterBean: standardJmsMessageConverter 116 | 117 | containers: 118 | transacted: 119 | meta: 120 | parentBean: standardJmsListenerContainer 121 | transactionManagerBean: transactionManager 122 | sessionTransacted: true 123 | standard: 124 | concurrentConsumers: 1 125 | subscriptionDurable: false 126 | autoStartup: false 127 | connectionFactoryBean: jmsConnectionFactory 128 | cacheLevel: 3 129 | 130 | adapters: 131 | standard: 132 | messageConverterBean: standardJmsMessageConverter 133 | persistenceInterceptorBean: persistenceInterceptor 134 | 135 | destinations: 136 | named: 137 | topic: 138 | key: 'conf.named.topic' 139 | queue: 140 | key: 'conf.named.queue' 141 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/JmsGrailsPlugin.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.jms 2 | 3 | import grails.plugin.jms.bean.DefaultJmsBeans 4 | import grails.plugin.jms.bean.JmsBeanDefinitionsBuilder 5 | import grails.plugin.jms.listener.ListenerConfigFactory 6 | import grails.plugin.jms.listener.ServiceInspector 7 | import grails.plugins.Plugin 8 | import grails.util.Environment 9 | import groovy.util.logging.Commons 10 | import org.springframework.jms.support.converter.SimpleMessageConverter 11 | 12 | @Commons 13 | class JmsGrailsPlugin extends Plugin { 14 | 15 | // the version or versions of Grails the plugin is designed for 16 | def grailsVersion = "5.2.0 > *" 17 | 18 | // resources that are excluded from plugin packaging 19 | def pluginExcludes = [ 20 | "grails-app/views/error.gsp" 21 | ] 22 | 23 | def title = "Jms" // Headline display name of the plugin 24 | def author = "Jeff Brown" 25 | def authorEmail = "brownj@ociweb.com" 26 | def description = '''\ 27 | JMS integration for Grails. 28 | ''' 29 | def profiles = ['web'] 30 | 31 | def documentation = "http://grails.org/plugin/jms" 32 | 33 | def license = "APACHE" 34 | 35 | def developers = [[name: "Weiqi Gao", email: "gaow@ociweb.com"], 36 | [name: "Søren Berg Glasius", email: "soeren+jmsplugin@glasius.dk"] ] 37 | 38 | def issueManagement = [system: "GitHub", url: "https://github.com/gpc/jms/issues"] 39 | 40 | def scm = [url: "https://github.com/gpc/jms"] 41 | 42 | def loadAfter = ['services', 'controllers', 'domainClass', 'dataSource', 'hibernate', 'hibernate4', 'hibernate5'] 43 | 44 | def serviceInspector = new ServiceInspector() 45 | def listenerConfigs = [:] 46 | def listenerConfigFactory = new ListenerConfigFactory() 47 | 48 | Closure doWithSpring() { 49 | { -> 50 | 51 | def jmsConfig = getJmsConfigurationWithDefaults() 52 | 53 | log.debug("merged config: $jmsConfig") 54 | if (jmsConfig.disabled) { 55 | log.warn("not registering listeners because JMS is disabled") 56 | return 57 | } 58 | 59 | new JmsBeanDefinitionsBuilder(jmsConfig).build(delegate) 60 | 61 | // TODO 62 | standardJmsMessageConverter SimpleMessageConverter 63 | 64 | grailsApplication.serviceClasses?.each { service -> 65 | def serviceClass = service.getClazz() 66 | def serviceClassListenerConfigs = getListenerConfigs(serviceClass, grailsApplication) 67 | if (serviceClassListenerConfigs) { 68 | serviceClassListenerConfigs.each { 69 | registerListenerConfig(it, delegate) 70 | } 71 | listenerConfigs[serviceClass.name] = serviceClassListenerConfigs 72 | } 73 | } 74 | } 75 | } 76 | 77 | def getJmsConfigurationWithDefaults() { 78 | if(config.jms) { 79 | ConfigObject jmsConfig = new ConfigObject() 80 | jmsConfig.putAll(config.jms) 81 | 82 | return getDefaultConfig().merge(jmsConfig) 83 | } 84 | else { 85 | return getDefaultConfig() 86 | } 87 | } 88 | 89 | def getDefaultConfig() { 90 | new ConfigSlurper(Environment.current.name).parse(DefaultJmsBeans) 91 | } 92 | 93 | def getListenerConfigs(serviceClass, application) { 94 | log.debug("inspecting '${serviceClass.name}' for JMS listeners") 95 | serviceInspector.getListenerConfigs(serviceClass, listenerConfigFactory, application) 96 | } 97 | 98 | def registerListenerConfig(listenerConfig, beanBuilder) { 99 | def queueOrTopic = (listenerConfig.topic) ? "TOPIC" : "QUEUE" 100 | log.info "registering listener for '${listenerConfig.listenerMethodName}' of service '${listenerConfig.serviceBeanPrefix}' to ${queueOrTopic} '${listenerConfig.destinationName}'" 101 | listenerConfig.register(beanBuilder) 102 | } 103 | 104 | void doWithApplicationContext() { 105 | listenerConfigs.each { serviceClassName, serviceClassListenerConfigs -> 106 | serviceClassListenerConfigs.each { 107 | startListenerContainer(it, applicationContext) 108 | } 109 | } 110 | //Fetch and set the asyncReceiverExecutor 111 | try { 112 | def asyncReceiverExecutor = applicationContext.getBean('jmsAsyncReceiverExecutor') 113 | if (asyncReceiverExecutor) { 114 | log.info "A jmsAsyncReceiverExecutor was detected in the Application Context and therefore will be set in the JmsService." 115 | applicationContext.getBean('jmsService').asyncReceiverExecutor = asyncReceiverExecutor 116 | } 117 | } 118 | catch (e) { 119 | log.debug "No jmsAsyncReceiverExecutor was detected in the Application Context." 120 | } 121 | 122 | } 123 | 124 | 125 | def startListenerContainer(listenerConfig, applicationContext) { 126 | def listenerContainer = applicationContext.getBean(listenerConfig.listenerContainerBeanName) 127 | 128 | if (listenerContainer.isAutoStartup()) { 129 | listenerContainer.start() 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/docs/asciidoc/receivingMessagesWithSelectors.adoc: -------------------------------------------------------------------------------- 1 | As mentioned in <> this plugin adds a service called `jmsService` to your application. 2 | In addition to the methods already described in other chapters the `jmsService` has the following methods that can be used to receive a selected message as a single operation without a *Service Listener*. 3 | 4 | 5 | === The receiveSelected(destination, selector, timeout, jmsTemplateBeanName) 6 | 7 | *destination* 8 | 9 | An instance of `javax.jms.Destination` , `javax.jms.Topic` , a `String` or a `Map` . 10 | 11 | A `String` destination argument will be interpreted as the name of a destination _queue_ . 12 | 13 | A `Map` destination argument can be used in the following ways: 14 | 15 | [source,groovy] 16 | ---- 17 | // Expect/Receive a message with a *selector* on a literal queue waiting up to the given *timeout*. 18 | // Will return the converted message or null if the message was not available. 19 | jmsService.receiveSelected(queue: "aQueue", selector, timeout, "standard") 20 | 21 | 22 | // Expect/Receive a message with a *selector* on a literal topic waiting up to the given *timeout*. 23 | // Will return the converted message or null if the message was not available. 24 | jmsService.receiveSelected(topic: "aTopic", selector, timeout, "standard") 25 | 26 | 27 | // Expect/Receive a message with a *selector* on the queue '«appname».person' waiting up to the given *timeout*. 28 | // Will return the converted message or null if the message was not available. 29 | jmsService.receiveSelected(service: "person", selector, timeout, "standard") 30 | 31 | // Expect/Receive a message with a *selector* on the queue '«appname».person.doIt' waiting up to the given *timeout*. 32 | // Will return the converted message or null if the message was not available. 33 | jmsService.receiveSelected(service: "person", method: "doIt", selector, timeout, "standard") 34 | 35 | 36 | // Expect/Receive a message with a *selector* on the queue 'remote.person.doIt' waiting up to the given *timeout*. 37 | // Will return the converted message or null if the message was not available. 38 | jmsService.receiveSelected(app: "remote", service: "person", method: "doIt", selector, timeout, "standard") 39 | ---- 40 | 41 | *selector* 42 | 43 | This is the message selector as described by the JMS Specification. 44 | In a nutshell a *message selector* lets a client specify a statement, which is similar to an SQL92 statement, that will be used to filter messages through the values of their *message headers* and *message properties*. 45 | "Only messages whose header and property values match the selector are delivered". 46 | As described in the *JMS* Specification what it means for a message not to be delivered depends on the MessageConsumer being used. 47 | It is important to mention that the selectors can only access *header* or *properties* but will *not be able to access any message body values*. 48 | 49 | 50 | ==== References 51 | 52 | http://download.oracle.com/javaee/1.3/api/javax/jms/Message.html[JavaEE 1.3 javax.jms.Message] 53 | 54 | http://activemq.apache.org/selectors.html[ActiveMq Selectors] 55 | 56 | http://publib.boulder.ibm.com/infocenter/wmbhelp/v6r1m0/topic/com.ibm.etools.mft.doc/ac24876_.htm[IBM Guide on Selectors] 57 | 58 | 59 | *timeout* 60 | 61 | A *long* value that specifies the amount of milliseconds that this call should wait until desisting and returning `null`. 62 | 63 | *jmsTemplateName* 64 | 65 | The name of the template that should be used to send the message. 66 | If this value is `null` , the standard template will be used (called "standard"). 67 | 68 | There are variations of the receiveSelected() method for convenience... 69 | 70 | === receiveSelected() method variants 71 | 72 | [source,java] 73 | ---- 74 | jmsService.receiveSelected(destination, selector) // use the default timeout and standard template 75 | jmsService.receiveSelected(destination, selector, timeout) // use the standard template 76 | ---- 77 | 78 | === Specifying a timeout through configuration or the template. 79 | 80 | If no *timeout* is specified the JmsService uses a **500** millisecond timeout. 81 | You can also specify a timeout through the `Config.groovy` file. 82 | 83 | [source,:java] 84 | ---- 85 | //Specifying a 100 milliseconds timeout 86 | jms.receiveTimeout=100l 87 | ---- 88 | 89 | Or if you are providing a custom `JmsTemplate` through its `receiveTimeout` attribute. 90 | 91 | **Note: Both timeouts will be ignored if set to zero, the only way of setting a zero timeout would be by passing such timeout as an argument to the call. 92 | 93 | === The receiveSelectedAsync(destination, selector, timeout, jmsTemplateBeanName) 94 | 95 | This methods provides a variant to the `receiveSelected` method, the difference is that this method will execute the request asynchronously by wrapping a call to the `receiveSelected` within an *Executor Service* (see `java.util.concurrent.ExecutorService` in your JDK API 1.5+ ). 96 | 97 | Some examples.. 98 | 99 | [source,java] 100 | ---- 101 | // Expect/Receive a message with a *selector* on a literal queue waiting up to the given *timeout*. 102 | // Will return a java.util.concurrent.Future that holds the result of the asynchronous execution. 103 | java.util.concurrent.Future afuture = jmsService.receiveSelectedAsync(queue: "aQueue", selector, timeout, "standard") 104 | 105 | // Expect/Receive a message with a *selector* on a literal topic waiting up to the given *timeout*. 106 | // Will return a java.util.concurrent.Future that holds the result of the asynchronous execution. 107 | java.util.concurrent.Future afuture = jmsService.receiveSelectedAsync(topic: "aTopic", selector, timeout, "standard") 108 | ---- 109 | -------------------------------------------------------------------------------- /src/docs/asciidoc/browsingMessagesInQueue.adoc: -------------------------------------------------------------------------------- 1 | If you are looking on ways to obtain the contents of a given `javax.jms.Queue` without changing its state the `JmsService` offers a set of methods designed for this task. 2 | 3 | 4 | === The browse(destination, jmsTemplateName, browserCallback) method. 5 | 6 | Will retrieve all messages inside the given _queue_ at that time without changing its state i.e messages will not be consumed. 7 | This method will convert the `javax.jms.Message` using the `JmsTemplate`. 8 | If you need the `javax.jms.Message` you should use the `browseNoConvert()` method and its variants as described further on. 9 | 10 | *destination* 11 | 12 | An instance of `javax.jms.Queue` , a `String` or a `Map` . *Needs* to be a _queue_ . 13 | 14 | A `String` destination argument will be interpreted as the name of a destination _queue_ . 15 | 16 | A `Map` destination argument can be used in the following ways: 17 | 18 | [source,groovy] 19 | ---- 20 | // browse literal queue 21 | List messages = jmsService.browse(queue: "aQueue", "standard", null) 22 | 23 | // browse the queue '«appname».person' 24 | List messages = jmsService.browse(service: "person", "standard", null) 25 | 26 | // browse the queue '«appname».person.doIt' 27 | List messages = jmsService.browse(service: "person", method: "doIt", "standard", null) 28 | 29 | // browse the queue 'remote.person.doIt' 30 | List messages = jmsService.browse(app: "remote", service: "person", method: "doIt", "standard", null) 31 | ---- 32 | 33 | *jmsTemplateName* 34 | 35 | The name of the template that should be used to send the message. 36 | If this value is `null` , the standard template will be used (called "standard"). 37 | 38 | *browserCallback* 39 | 40 | An optional closure that can be used to "process" the message before being added to the returning message list. 41 | The value returned by this *callback* will be the one added to the returning list if such value is *not null*. 42 | 43 | === browse() method variants 44 | 45 | There are variations of the browse() method for convenience... 46 | 47 | [source,java] 48 | ---- 49 | List messages = jmsService.browse(queue) 50 | 51 | List messages = jmsService.browse(queue, browserCallback) 52 | 53 | List messages = jmsService.browse(queue, jmsTemplateBeanName) 54 | ---- 55 | 56 | === The browseNoConvert(destination, jmsTemplateName, browserCallback) method. 57 | 58 | This method will not convert the `javax.jms.Message`. 59 | In other words the `browserCallback:Closure` will receive a `javax.jms.Message` or if no _callback_ is defined a _list_ containing `javax.jms.Message` instances will be returned. 60 | *You can't* update the returned `javax.jms.Message` objects, they are *read-only* instances. 61 | 62 | [source,java] 63 | ---- 64 | List messages = jmsService.browseNoConvert(queue) 65 | 66 | //You can do the following to filter messages or use a selector through the browseSelected* methods 67 | List messages = jmsService.browseNoConvert(queue){ javax.jms.Message msg -> 68 | ( msg.getStringProperty('aproperty') ? msg : null ) 69 | } 70 | 71 | List messages = jmsService.browseNoConvert(queue, jmsTemplateBeanName) 72 | messages.each { 73 | assert it instanceof javax.jms.Message 74 | } 75 | ---- 76 | 77 | === The browseSelected(destination, selector, jmsTemplateName, browserCallback) method. 78 | 79 | Will retrieve messages that match the *selector* inside the given _queue_ at the time of the call without changing its state i.e messages will not be consumed. 80 | This method will convert the `javax.jms.Message` using the `JmsTemplate`. 81 | If you need the `javax.jms.Message` you should use the `browseSelectedNotConvert()` method and its variants as described further on. 82 | 83 | *selector* 84 | 85 | This is the message selector as described by the JMS Specification. 86 | In a nutshell a *message selector* lets a client specify a statement, which is similar to an SQL92 statement, that will be used to filter messages through the values of their *message headers* and *message properties*. 87 | "Only messages whose header and property values match the selector are delivered". 88 | As described in the *JMS* Specification what it means for a message not to be delivered depends on the MessageConsumer being used. 89 | It is important to mention that the selectors can only access *header* or *properties* but will *not be able to access any message body values*. 90 | 91 | [source,java] 92 | ---- 93 | List messages = jmsService.browseSelected(queue, " anIntProperty > 0 AND anotherProperty='a Value'") 94 | 95 | //filtering through body content. 96 | List messages = jmsService.browseSelected(queue, " anIntProperty > 0 AND anotherProperty='a Value'"){ 97 | ( msg == 'avalue' ?: null ) 98 | } 99 | 100 | List messages = jmsService.browseSelected(queue, " anIntProperty > 0 AND anotherProperty='a Value'", jmsTemplateBeanName) 101 | ---- 102 | 103 | === The browseSelectedNotConvert(destination, selector, jmsTemplateName, browserCallback) method. 104 | 105 | Will retrieve messages that match the *selector* inside the given _queue_ at the time of the call without changing its state i.e messages will not be consumed. 106 | As the `browseNoConvert` this method will not convert the `javax.jms.Message`. 107 | 108 | [source,java] 109 | ---- 110 | List messages = jmsService.browseSelectedNotConvert(queue, " anIntProperty > 0 AND anotherProperty='a Value'") 111 | 112 | List messages = jmsService.browseSelectedNotConvert(queue, " anIntProperty > 0 AND anotherProperty='a Value'"){ javax.jms.Message msg -> 113 | return msg.JMSCorrelationID 114 | } 115 | 116 | List messages = jmsService.browseSelectedNotConvert(queue, " anIntProperty > 0 AND anotherProperty='a Value'", jmsTemplateBeanName) 117 | messages.each { 118 | assert it instanceof javax.jms.Message 119 | } 120 | ---- 121 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/listener/ServiceInspector.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Grails Plugin Collective 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package grails.plugin.jms.listener 17 | 18 | import grails.boot.GrailsApp 19 | import grails.core.GrailsApplication 20 | import grails.plugin.jms.Queue 21 | import grails.plugin.jms.Subscriber 22 | import groovy.util.logging.Slf4j 23 | import org.apache.commons.logging.Log 24 | import org.apache.commons.logging.LogFactory 25 | import grails.util.GrailsClassUtils 26 | 27 | @Slf4j 28 | class ServiceInspector { 29 | 30 | static final String SERVICE_LISTENER_METHOD = "onMessage" 31 | static final String EXPOSES_SPECIFIER = "exposes" 32 | static final String EXPOSE_SPECIFIER = "expose" 33 | static final String EXPOSES_JMS_SPECIFIER = "jms" 34 | 35 | def getListenerConfigs(service, listenerConfigFactory, GrailsApplication grailsApplication) { 36 | if (!exposesJms(service)) return [] 37 | 38 | def listenerConfigs = [] 39 | 40 | listenerConfigs << getServiceListenerConfig(service, listenerConfigFactory, grailsApplication) 41 | service.methods.findAll { !it.synthetic }.each { 42 | listenerConfigs << getServiceMethodListenerConfig(service, it, listenerConfigFactory, grailsApplication) 43 | } 44 | 45 | listenerConfigs.findAll { it != null } 46 | } 47 | 48 | def getServiceListenerConfig(service, listenerConfigFactory, GrailsApplication grailsApplication) { 49 | def hasServiceListenerMethod = hasServiceListenerMethod(service) 50 | if (hasServiceListenerMethod) { 51 | def listenerConfig = listenerConfigFactory.getListenerConfig(service, grailsApplication) 52 | listenerConfig.with { 53 | serviceListener = true 54 | listenerMethodName = SERVICE_LISTENER_METHOD 55 | explicitDestinationName = GrailsClassUtils.getStaticPropertyValue(service, "destination") 56 | topic = GrailsClassUtils.getStaticPropertyValue(service, "isTopic") ?: false 57 | messageSelector = GrailsClassUtils.getStaticPropertyValue(service, "selector") 58 | containerParent = GrailsClassUtils.getStaticPropertyValue(service, "container") ?: "standard" 59 | adapterParent = GrailsClassUtils.getStaticPropertyValue(service, "adapter") ?: "standard" 60 | } 61 | listenerConfig 62 | } 63 | } 64 | 65 | def hasServiceListenerMethod(service) { 66 | service.metaClass.methods.find { it.name == SERVICE_LISTENER_METHOD && it.parameterTypes.size() == 1 } != null 67 | } 68 | 69 | def exposesJms(service) { 70 | GrailsClassUtils.getStaticPropertyValue(service, EXPOSES_SPECIFIER)?. 71 | contains(EXPOSES_JMS_SPECIFIER) || 72 | GrailsClassUtils.getStaticPropertyValue(service, EXPOSE_SPECIFIER)?. 73 | contains(EXPOSES_JMS_SPECIFIER) 74 | } 75 | 76 | def isSingleton(service) { 77 | def scope = GrailsClassUtils.getStaticPropertyValue(service, 'scope') 78 | (scope == null || scope == "singleton") 79 | } 80 | 81 | def getServiceMethodListenerConfig(service, method, listenerConfigFactory, grailsApplication) { 82 | def subscriberAnnotation = method.getAnnotation(Subscriber) 83 | def queueAnnotation = method.getAnnotation(Queue) 84 | 85 | if (subscriberAnnotation) { 86 | getServiceMethodSubscriberListenerConfig(service, method, subscriberAnnotation, listenerConfigFactory, grailsApplication) 87 | } 88 | else if (queueAnnotation) { 89 | getServiceMethodQueueListenerConfig(service, method, queueAnnotation, listenerConfigFactory, grailsApplication) 90 | } 91 | } 92 | 93 | def getServiceMethodSubscriberListenerConfig(service, method, annotation, listenerConfigFactory, GrailsApplication grailsApplication) { 94 | def listenerConfig = listenerConfigFactory.getListenerConfig(service, grailsApplication) 95 | listenerConfig.with { 96 | topic = true 97 | listenerMethodName = method.name 98 | explicitDestinationName = resolveDestinationName(annotation.topic(), grailsApplication) 99 | messageSelector = annotation.selector() 100 | containerParent = annotation.container() 101 | adapterParent = annotation.adapter() 102 | } 103 | listenerConfig 104 | } 105 | 106 | def getServiceMethodQueueListenerConfig(service, method, annotation, listenerConfigFactory, GrailsApplication grailsApplication) { 107 | def listenerConfig = listenerConfigFactory.getListenerConfig(service, grailsApplication) 108 | listenerConfig.with { 109 | topic = false 110 | listenerMethodName = method.name 111 | explicitDestinationName = resolveDestinationName(annotation.name(), grailsApplication) 112 | messageSelector = annotation.selector() 113 | containerParent = annotation.container() 114 | adapterParent = annotation.adapter() 115 | } 116 | listenerConfig 117 | } 118 | 119 | String resolveDestinationName(final String name, GrailsApplication grailsApplication) { 120 | String resolvedName = name 121 | if ( resolvedName =~ /^\$/ ) { 122 | final String pathTokens = resolvedName.substring(1).tokenize('.').join('.') 123 | resolvedName = grailsApplication.config.getProperty('jms.destinations.'+pathTokens, String) 124 | if ( resolvedName ) { 125 | log.info "key '$name' resolved to destination '$resolvedName'." + 126 | "The name '$resolvedName' will be used as the destination." 127 | } 128 | else { 129 | throw new IllegalArgumentException( 130 | "The destination key '$name' is not available in the 'jms.destinations' configuration space." + 131 | "Please define such key or remove the prefix '\$' from the name.") 132 | } 133 | } 134 | resolvedName 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/jms/connection/JmsFallbackConnectionFactory.java: -------------------------------------------------------------------------------- 1 | package grails.plugin.jms.connection; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.util.concurrent.atomic.AtomicInteger; 5 | import java.util.concurrent.atomic.AtomicReference; 6 | 7 | import javax.jms.Connection; 8 | import javax.jms.ConnectionFactory; 9 | import javax.jms.JMSException; 10 | 11 | import org.apache.commons.logging.Log; 12 | import org.apache.commons.logging.LogFactory; 13 | 14 | /** 15 | * A connection factory that falls back to another connection factory if the primary 16 | * connection factory fails. 17 | *

 

18 | * If you do not assign a fallback JMS queue, it will try to use a 19 | * local (i.e.: JVM-resident in-memory) 20 | * ActiveMQ connection, assuming that {@code org.apache.activemq.ActiveMQConnectionFactory} is 21 | * a class on the class path. (See 22 | * 23 | * the ActiveMQ download page's Maven config area for the information necessary to pull it down.) 24 | *

 

25 | * This class is thread-safe, in that it is safe to change the parent no matter what other 26 | * activity may be going on. 27 | */ 28 | public class JmsFallbackConnectionFactory implements ConnectionFactory { 29 | 30 | private final Log log = LogFactory.getLog(getClass()); 31 | 32 | private static final AtomicInteger COUNTER = new AtomicInteger(0); 33 | 34 | private final AtomicReference parentRef = new AtomicReference(null); 35 | private final AtomicReference fallbackRef = new AtomicReference(null); 36 | 37 | public JmsFallbackConnectionFactory() { 38 | if (log.isTraceEnabled()) log.trace("Creating a " + getClass().getSimpleName() + " instance without a primary"); 39 | } 40 | 41 | public JmsFallbackConnectionFactory(ConnectionFactory parent) { 42 | this(); 43 | if (log.isTraceEnabled()) log.trace("Creating a " + getClass().getSimpleName() + " instance with primary [" + parent + "]"); 44 | setParent(parent); 45 | } 46 | 47 | public void setParent(ConnectionFactory parent) { 48 | if(parent == null) { 49 | if (log.isWarnEnabled()) log.warn("Assigning a null parent to a " + getClass().getSimpleName() + ": will always use local fallback JMS"); 50 | parentRef.set(null); 51 | } else { 52 | if (log.isDebugEnabled()) log.debug("Setting a non-null parent to a " + getClass().getSimpleName() + ": [" + parent + "]"); 53 | parentRef.set(new WrappedParentConnectionFactory(parent)); 54 | } 55 | } 56 | 57 | public ConnectionFactory getParent() { 58 | WrappedParentConnectionFactory toReturn = parentRef.get(); 59 | if (toReturn == null) { 60 | if (log.isInfoEnabled()) log.info("Returning a null parent from a " + getClass().getSimpleName()); 61 | return null; 62 | } 63 | return toReturn.unwrap(); 64 | } 65 | 66 | public void setFallback(ConnectionFactory fallback) { 67 | if(fallback == null) { 68 | if (log.isWarnEnabled()) log.warn("Assigning a null fallback to a " + getClass().getSimpleName() + ": will always use local fallback JMS"); 69 | } 70 | fallbackRef.set(fallback); 71 | } 72 | 73 | public ConnectionFactory getFallback() { 74 | ConnectionFactory toReturn = fallbackRef.get(); 75 | if(toReturn == null) { 76 | if (log.isInfoEnabled()) log.info("Creating a local fallback JMS queue"); 77 | toReturn = createLocalFallback(); 78 | if(!fallbackRef.compareAndSet(null, toReturn)) { 79 | if (log.isInfoEnabled()) log.info("Detected that the fallback JMS queue was set while initializing a local fallback; trying to get fallback again"); 80 | return getFallback(); 81 | } 82 | } 83 | return toReturn; 84 | } 85 | 86 | @SuppressWarnings("unchecked") 87 | protected ConnectionFactory createLocalFallback() { 88 | final String className = "org.apache.activemq.ActiveMQConnectionFactory"; 89 | final Class clazz; 90 | try { 91 | clazz = (Class)Class.forName("org.apache.activemq.ActiveMQConnectionFactory"); 92 | } catch(ClassNotFoundException cnfe) { 93 | if (log.isWarnEnabled()) log.warn("Could not find " + className + ", so cannot create the in-memory fallback queue"); 94 | return null; 95 | } 96 | 97 | final Constructor constructor; 98 | try { 99 | constructor = clazz.getConstructor(String.class); 100 | } catch(NoSuchMethodException ex) { 101 | throw new RuntimeException("Serious implementation error: could not find expected String constructor on " + clazz.getName()); 102 | } 103 | 104 | final String connStr = 105 | "vm://localhost?broker.persistent=false&broker.useShutdownHook=false&broker.brokerName=fallback-" + 106 | COUNTER.incrementAndGet(); 107 | 108 | try { 109 | return constructor.newInstance(connStr); 110 | } catch(Exception ex) { 111 | throw new RuntimeException("Could not construct an instance of " + clazz.getName() + " using String \"" + connStr + "\"", ex); 112 | } 113 | } 114 | 115 | public Connection createConnection() throws JMSException { 116 | ConnectionFactory toUse = getParent(); 117 | if(toUse == null) { 118 | toUse = getFallback(); 119 | if(toUse == null) { 120 | throw new IllegalStateException("Could not retrieve either a parent or fallback Connection Factory"); 121 | } 122 | if (log.isInfoEnabled()) log.info("Using fallback JMS connection factory"); 123 | } 124 | return toUse.createConnection(); 125 | } 126 | 127 | public Connection createConnection(String userName, String password) throws JMSException { 128 | ConnectionFactory toUse = getParent(); 129 | if(toUse == null) { 130 | toUse = getFallback(); 131 | if(toUse == null) { 132 | throw new IllegalStateException("Could not retrieve either a parent or fallback Connection Factory"); 133 | } 134 | if (log.isInfoEnabled()) log.info("Using fallback JMS connection factory"); 135 | } 136 | return toUse.createConnection(userName, password); 137 | } 138 | 139 | public class WrappedParentConnectionFactory implements ConnectionFactory { 140 | 141 | private final ConnectionFactory source; 142 | 143 | public WrappedParentConnectionFactory(ConnectionFactory source) { 144 | if(source == null) throw new IllegalArgumentException("Connection Factory to wrap cannot be null"); 145 | this.source = source; 146 | } 147 | 148 | /** 149 | * Attempts to create a connection; failing that, 150 | * attempts to create a fallback connection. 151 | */ 152 | public Connection createConnection() throws JMSException { 153 | try { 154 | return source.createConnection(); 155 | } catch(Exception e) { 156 | if (log.isInfoEnabled()) log.info("Error in connecting to the JMS queue; reverting to fallback JMS connection", e); 157 | } 158 | return getFallback().createConnection(); 159 | } 160 | 161 | /** 162 | * Attempts to create a connection with the given username and password; failing that, 163 | * attempts to create a fallback connection with the given username and password; failing 164 | * that, attempts to create a fallback connection without a username or password. 165 | */ 166 | public Connection createConnection(String userName, String password) throws JMSException { 167 | try { 168 | return source.createConnection(userName, password); 169 | } catch(Exception e) { 170 | if (log.isInfoEnabled()) log.info("Error in connecting to the JMS queue; reverting to fallback JMS connection", e); 171 | } 172 | try { 173 | return getFallback().createConnection(userName, password); 174 | } catch(Exception e) { 175 | if (log.isInfoEnabled()) log.info("Reverting to fallback JMS connection without username or password", e); 176 | } 177 | return getFallback().createConnection(); 178 | } 179 | 180 | /** 181 | * Provides the underlying parent connection factory. 182 | * @return the connection factory 183 | */ 184 | public ConnectionFactory unwrap() { 185 | return source; 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /grails-app/services/grails/plugin/jms/JmsService.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.jms 2 | 3 | import grails.core.GrailsApplication 4 | import grails.plugin.jms.listener.GrailsMessagePostProcessor 5 | import groovy.util.logging.Slf4j 6 | import org.springframework.jms.core.BrowserCallback 7 | import org.springframework.jms.core.JmsTemplate 8 | import org.springframework.jms.core.MessagePostProcessor 9 | import org.springframework.jms.support.JmsUtils 10 | 11 | import javax.annotation.PreDestroy 12 | import javax.jms.Destination 13 | import javax.jms.JMSException 14 | import javax.jms.Message 15 | import javax.jms.QueueBrowser 16 | import javax.jms.Session 17 | import javax.jms.Topic 18 | import java.util.concurrent.Callable 19 | import java.util.concurrent.ExecutorService 20 | import java.util.concurrent.Executors 21 | import java.util.concurrent.Future 22 | import java.util.concurrent.TimeUnit 23 | import java.util.concurrent.locks.Lock 24 | import java.util.concurrent.locks.ReentrantLock 25 | 26 | @Slf4j 27 | class JmsService { 28 | 29 | public static final String DEFAULT_JMS_TEMPLATE_BEAN_NAME = "standard" 30 | public static final long DEFAULT_RECEIVER_TIMEOUT_MILLIS = 500 31 | 32 | GrailsApplication grailsApplication 33 | 34 | ExecutorService asyncReceiverExecutor 35 | boolean asyncReceiverExecutorShutdown = true 36 | private Lock asyncReceiverExecutorCreateLock = new ReentrantLock() 37 | 38 | //-- Life Cycle -------------- 39 | 40 | @PreDestroy 41 | void destroy() { 42 | doWithinAsyncLock { 43 | if (this.@asyncReceiverExecutor) { 44 | if (asyncReceiverExecutorShutdown) { 45 | shutdownAsyncReceiverExecutorNow() 46 | } 47 | else { 48 | log.info "The flag to shutdown the Async. Executor is turned off. The executor will not be terminated" 49 | } 50 | } 51 | } 52 | } 53 | 54 | private void shutdownAsyncReceiverExecutorNow() { 55 | log.info "Shutting down current Async. Executor..." 56 | try { 57 | def runnables = asyncReceiverExecutor.shutdownNow() 58 | if (runnables && runnables.size() > 0) { 59 | log.warn "Async. Executor Shutting down with ${runnables.size()} pending tasks." 60 | } 61 | } 62 | catch (e) { 63 | log.error "Error while shutting down Async. Executor: $e.message." 64 | } 65 | } 66 | 67 | //-- Receivers --------------- 68 | 69 | def receiveSelected(destination, selector, String jmsTemplateBeanName) { 70 | receiveSelected(destination, selector, null, jmsTemplateBeanName) 71 | } 72 | 73 | /** 74 | *
75 | * Receive and converts a message synchronously from the default destination, but only wait up to a specified time for delivery. 76 | * This method should be used carefully, since it will block the thread until the message becomes available or until the timeout value is exceeded. 77 | *
78 | * description copied from interface org.springframework.jms.core.JmsOperations. 79 | * 80 | * The big difference between the {@code receiveSelected} methods provided by the org.springframework.jms.core.JmsOperations is that we to a 81 | * message conversion and we try to enforce a timeout. Such timeout is defined by the following rules 82 | * described in the method. {@code JmsService # calculatedReceiverTimeout}. 83 | * 84 | *
    85 | *
  1. argument timeout: Selected if the value directly sent as argument is not null.
  2. 86 | *
  3. jmsTemplate.receiverTimeout: Selected if the value of the {@code template.receiverTimeout} is different 87 | * from {@link JmsTemplate#RECEIVE_TIMEOUT_INDEFINITE_WAIT} (or zero).
  4. 88 | *
  5. If the value provided by {@code config.jms.receiveTimeout} is not null and different 89 | * from {@link JmsTemplate#RECEIVE_TIMEOUT_INDEFINITE_WAIT}.
  6. 90 | *
  7. A default value of {@link #DEFAULT_RECEIVER_TIMEOUT_MILLIS} is used if none of the above are selected.
  8. 91 | *
92 | */ 93 | def receiveSelected(destination, selector, Long timeout = null, String jmsTemplateBeanName = null) { 94 | if (disabled) { 95 | log.warn "will not receive over [$destination] because JMS is disabled in config" 96 | return 97 | } 98 | 99 | final ctx = normalizeServiceCtx(destination, jmsTemplateBeanName) 100 | 101 | logAction "Awaiting for JMS message with selector '$selector' from ", ctx 102 | 103 | def log = this.log 104 | ctx.with { 105 | jmsTemplate.receiveTimeout = calculatedReceiverTimeout(timeout, jmsTemplate) 106 | log.debug "JMS Template receiver timeout set to ${jmsTemplate.receiveTimeout}" 107 | 108 | logAction "Receivng JMS message with selector '$selector' from ", ctx 109 | def msg = jmsTemplate.receiveSelectedAndConvert(ndestination, selector) 110 | 111 | log.debug "Received JMS message with selector '$selector': $msg" 112 | msg 113 | } 114 | } 115 | 116 | Future receiveSelectedAsync(destination, selector, String jmsTemplateBeanName) { 117 | receiveSelectedAsync(destination, selector, null, postProcessor) 118 | } 119 | 120 | /** 121 | * Submits a {@code receiveSelected} call through an {@link java.util.concurrent.Executor} and returns a future 122 | * that reflects the execution of the task. The {@code Executor} is provided by {@code JmsService.getJmsAsyncReceiverExecutor ( )}. 123 | */ 124 | Future receiveSelectedAsync(destination, selector, Long timeout = null, String jmsTemplateBeanName = null) { 125 | if (disabled) { 126 | log.warn "will not receive from [$destination] with selector [$selector] because JMS is disabled in config" 127 | return 128 | } 129 | 130 | log.debug "Submitting Async Selected Receiver for [$destination] with selector [$selector].." 131 | getAsyncReceiverExecutor().submit({ receiveSelected(destination, selector, timeout) } as Callable) 132 | } 133 | 134 | //-- Senders --------------- 135 | 136 | def send(destination, message, Closure callback) { 137 | send(destination, message, null, callback) 138 | } 139 | 140 | def send(destination, message, String jmsTemplateBeanName = null, Closure callback = null) { 141 | if (disabled) { 142 | log.warn "not sending message [$message] to [$destination] because JMS is disabled in config" 143 | return 144 | } 145 | 146 | def ctx = normalizeServiceCtx(destination, jmsTemplateBeanName) 147 | logAction "Sending JMS message '$message' to ", ctx 148 | 149 | ctx.with { 150 | if (callback) { 151 | jmsTemplate.convertAndSend(ndestination, message, toMessagePostProcessor(jmsTemplate, callback)) 152 | } 153 | else { 154 | jmsTemplate.convertAndSend(ndestination, message) 155 | } 156 | } 157 | } 158 | 159 | protected MessagePostProcessor toMessagePostProcessor(JmsTemplate template, Closure callback) { 160 | new GrailsMessagePostProcessor(jmsService: this, jmsTemplate: template, processor: callback) 161 | } 162 | 163 | //-- Browsers --------------- 164 | /** 165 | * Returns a list with the messages inside the given queue. 166 | */ 167 | def browseNoConvert(queue, String jmsTemplateBeanName = null, Closure browserCallback = null) { 168 | doBrowseSelected(queue, null, jmsTemplateBeanName, false, browserCallback) 169 | } 170 | 171 | def browseNoConvert(queue, Closure browserCallback) { 172 | doBrowseSelected(queue, null, null, false, browserCallback) 173 | } 174 | 175 | /** 176 | * Returns a list with the messages inside the given queue. 177 | * The list will contain javax.jms.Message instances since no conversion will be attempted. 178 | */ 179 | def browse(queue, String jmsTemplateBeanName = null, Closure browserCallback = null) { 180 | doBrowseSelected(queue, null, jmsTemplateBeanName, true, browserCallback) 181 | } 182 | 183 | def browse(queue, Closure browserCallback) { 184 | doBrowseSelected(queue, null, null, true, browserCallback) 185 | } 186 | 187 | /** 188 | * Returns a list with the messages inside the given queue that match the given selector. 189 | * Messages will be converted using the Jms Template before being added to the list. 190 | */ 191 | def browseSelected(queue, selector, String jmsTemplateBeanName = null, Closure browserCallback = null) { 192 | doBrowseSelected(queue, selector, jmsTemplateBeanName, true, browserCallback) 193 | } 194 | 195 | def browseSelected(queue, selector, Closure browserCallback) { 196 | doBrowseSelected(queue, selector, null, true, browserCallback) 197 | } 198 | 199 | /** 200 | * Returns a list with the messages inside the given queue that match the given selector. 201 | * The list will contain javax.jms.Message instances since no conversion will be attempted. 202 | */ 203 | def browseSelectedNotConvert(queue, selector, String jmsTemplateBeanName = null, Closure browserCallback = null) { 204 | doBrowseSelected(queue, selector, jmsTemplateBeanName, false, browserCallback) 205 | } 206 | 207 | def browseSelectedNotConvert(queue, selector, Closure browserCallback) { 208 | doBrowseSelected(queue, selector, null, false, browserCallback) 209 | } 210 | 211 | /** 212 | * Delegate to all browse actions. It leverages the {@code org.springframework.jms.core.JmsTemplate.browseSelected} method. 213 | * This method accepts a JMS selector to filter the messages. You can also define a 214 | * browserCallback closure which will receive all messages, if defined its return value will be the one 215 | * added to the list of messages. If no browserCallback closure is specified the list 216 | * will contain the messages inside the given queue filtered only by the JMS selector if available. 217 | * 218 | * By default it will try to convert the messages according to the given JmsTemplate. 219 | * 220 | * Note:This method will throw an {@code IllegalArgumentException} if the destination is not a queue. 221 | * @param queue 222 | * @param selector 223 | * @param jmsTemplateBeanName 224 | * @param convert {@code true} if you want to convert the {@code javax.jms.Message}; {@code false} to receive the raw {@code javax.jms.Message} 225 | * @param browserCallback Closure that gets executed per message. If specified its return value will be the one added to the list that this method returns. 226 | */ 227 | private doBrowseSelected(queue, selector, String jmsTemplateBeanName = null, boolean convert = true, Closure browserCallback = null) { 228 | if (disabled) { 229 | if (selector) { 230 | log.warn "not browsing [$queue] with selector [$selector] because JMS is disabled in config" 231 | } 232 | else { 233 | log.warn "not browsing [$queue] because JMS is disabled in config" 234 | } 235 | return 236 | } 237 | 238 | def ctx = normalizeServiceCtx(queue, jmsTemplateBeanName) 239 | if (ctx.type != 'queue') { 240 | new IllegalArgumentException("The destination [$queue] must be a queue.") 241 | } 242 | 243 | if (selector) { 244 | logAction "Browsing messages with selector [$selector] ", ctx 245 | } 246 | else { 247 | logAction "Browsing messages ", ctx 248 | } 249 | 250 | final messages = [] as ArrayList 251 | 252 | ctx.with { 253 | def callback = { Session session, QueueBrowser browser -> 254 | for (Message m in browser.enumeration) { 255 | def processedMessage = convert ? convertMessageWithTemplate(jmsTemplate, m) : m 256 | if (browserCallback) { 257 | def val = browserCallback.call(processedMessage) 258 | if (val != null) { 259 | messages << val 260 | } 261 | } 262 | else { 263 | messages << (processedMessage) 264 | } 265 | } 266 | } as BrowserCallback 267 | 268 | jmsTemplate.browseSelected(ndestination, selector, callback) 269 | } 270 | 271 | messages 272 | } 273 | 274 | //-- Util --------------- 275 | 276 | boolean isDisabled() { 277 | grailsApplication.config.getProperty('jms.disabled', Boolean, false) 278 | } 279 | 280 | private convertMessageWithTemplate(template, Message message) { 281 | if (message) { 282 | def converter = template?.messageConverter 283 | try { 284 | converter?.fromMessage(message) 285 | } 286 | catch (JMSException ex) { 287 | throw JmsUtils.convertJmsAccessException(ex) 288 | } 289 | } 290 | } 291 | 292 | /** 293 | * Calculates the Receiver Timeout according to the following precedence. 294 | *
    295 | *
  1. argument callReceiveTimeout: Selected if the value directly sent as argument is not null.
  2. 296 | *
  3. jmsTemplate.receiverTimeout: Selected if the value of the {@code template.receiverTimeout} is different 297 | * from {@code JmsTemplate.RECEIVE_TIMEOUT_INDEFINITE_WAIT} (or zero).
  4. 298 | *
  5. Thre Grails Configuration mechanism provides a jms.receiveTimeout which value is not null and different 299 | * from {@code JmsTemplate.RECEIVE_TIMEOUT_INDEFINITE_WAIT} (or zero) .
  6. 300 | *
  7. A default value of {@link JmsService#DEFAULT_RECEIVER_TIMEOUT_MILLIS} is used if none of the above are selected.
  8. 301 | *
302 | */ 303 | long calculatedReceiverTimeout(callReceiveTimeout, jmsTemplate) { 304 | if (callReceiveTimeout != null) { 305 | return callReceiveTimeout 306 | } 307 | 308 | if (jmsTemplate.receiveTimeout != JmsTemplate.RECEIVE_TIMEOUT_INDEFINITE_WAIT) { 309 | return jmsTemplate.receiveTimeout 310 | } 311 | 312 | def configReceiveTimeout = grailsApplication.config?.jms?.receiveTimeout 313 | if (configReceiveTimeout != null 314 | && configReceiveTimeout instanceof Number 315 | && configReceiveTimeout != JmsTemplate.RECEIVE_TIMEOUT_INDEFINITE_WAIT) { 316 | return configReceiveTimeout 317 | } 318 | 319 | DEFAULT_RECEIVER_TIMEOUT_MILLIS 320 | } 321 | 322 | private doWithinAsyncLock(Closure closure) { 323 | if (asyncReceiverExecutorCreateLock.tryLock(500, TimeUnit.MILLISECONDS)) { 324 | try { 325 | closure.call() 326 | } 327 | finally { 328 | asyncReceiverExecutorCreateLock.unlock() 329 | } 330 | } 331 | } 332 | 333 | /** 334 | * Setter for the executor that handles Async. Receiving requests. 335 | * Warning this method will shutdown any previous executor regardless of the {@code asyncReceiverExecutorShutdown} flag. 336 | * The {@code asyncReceiverExecutor} will be internally initialized if no executor is set but an async. receiver is 337 | * requested. 338 | */ 339 | void setAsyncReceiverExecutor(ExecutorService executorService) { 340 | log.debug "attempting to set asyncReceiverExecutor $executorService ..." 341 | doWithinAsyncLock { 342 | if (this.@asyncReceiverExecutor && !this.@asyncReceiverExecutor.shutdown) { 343 | shutdownAsyncReceiverExecutorNow() 344 | } 345 | this.@asyncReceiverExecutor = executorService 346 | } 347 | } 348 | 349 | /** 350 | * Provides the executor that handles Async. Receiving requests. If no {@code asyncReceiverExecutor} is specified 351 | * a {@code Cached Thread Pool}, as provided by {@link java.util.concurrent.Executors#newCachedThreadPool()}, 352 | * will be used. 353 | */ 354 | ExecutorService getAsyncReceiverExecutor() { 355 | if (!asyncReceiverExecutor) { 356 | doWithinAsyncLock { 357 | if (this.@asyncReceiverExecutor == null) { 358 | log.debug "default to a Cached Thread Pool for Async Selected Receivers." 359 | this.@asyncReceiverExecutor = Executors.newCachedThreadPool() 360 | } 361 | } 362 | } 363 | asyncReceiverExecutor 364 | } 365 | 366 | /** 367 | * Normalizes the context for sending or receiving a message. 368 | *
    369 | *
  • jmsTemplate : org.springframework.jms.core.JmsTemplate instance. 370 | *
  • ndestination : Normalized Destination 371 | *
  • type : Type of Destination, either ['topic'|'queue'] 372 | *
  • jmsTemplateBeanName : Name of the bean used to retrieve the JmsTemplate. 373 | *
  • defaultTemplate : Boolean value that tells us if the JmsTemplate is the Default Template. 374 | *
375 | */ 376 | private normalizeServiceCtx(destination, final String jmsTemplateBeanName) { 377 | final String _jmsTemplateBeanName = "${ jmsTemplateBeanName ?: DEFAULT_JMS_TEMPLATE_BEAN_NAME }JmsTemplate" 378 | boolean defaultTemplate = _jmsTemplateBeanName == "${DEFAULT_JMS_TEMPLATE_BEAN_NAME}JmsTemplate" 379 | 380 | def jmsTemplate = grailsApplication.mainContext.getBean(_jmsTemplateBeanName) 381 | if (jmsTemplate == null) { 382 | throw new Error("Could not find bean with name '${_jmsTemplateBeanName}' to use as a JmsTemplate") 383 | } 384 | 385 | def isTopic 386 | if (destination instanceof Destination) { 387 | isTopic = destination instanceof Topic 388 | } 389 | else { 390 | def destinationMap = convertToDestinationMap(destination) 391 | isTopic = destinationMap.containsKey("topic") 392 | jmsTemplate.pubSubDomain = isTopic 393 | destination = (isTopic) ? destinationMap.topic : destinationMap.queue 394 | } 395 | 396 | [jmsTemplate: jmsTemplate, // org.springframework.jms.core.JmsTemplate 397 | ndestination: destination, // Normalized Destination 398 | type: (isTopic ? 'topic' : 'queue'), // Type of Destination [topic|queue] 399 | jmsTemplateBeanName: _jmsTemplateBeanName, // Name of the bean used to retrieve the JmsTemplate. 400 | defaultTemplate: defaultTemplate] // Boolean value that tells us if the JmsTemplate is the Default Template. 401 | } 402 | 403 | /** 404 | * Single point of entry to log an action. 405 | * @param action Description of the Action. 406 | * @param ctx Context of the action as provided by the {@link JmsService#normalizeServiceCtx(Object, String)} method. 407 | */ 408 | private void logAction(final String action, final ctx) { 409 | log.info(ctx.with { 410 | "$action $type '$ndestination'${defaultTemplate ? '' : " using template '$jmsTemplateBeanName'"}" 411 | }) 412 | } 413 | 414 | def convertToDestinationMap(destination) { 415 | if (destination == null) { 416 | [queue: null] 417 | } 418 | else if (destination instanceof String) { 419 | [queue: destination] 420 | } 421 | else if (destination instanceof Map) { 422 | if (destination.queue) { 423 | [queue: destination.queue] 424 | } 425 | else if (destination.topic) { 426 | [topic: destination.topic] 427 | } 428 | else { 429 | def parts = [] 430 | if (destination.app) { 431 | parts << destination.app 432 | } 433 | else { 434 | parts << grailsApplication.metadata['app.name'] 435 | } 436 | if (destination.service) { 437 | parts << destination.service 438 | if (destination.method) { 439 | parts << destination.method 440 | } 441 | } 442 | [queue: (parts) ? parts.join('.') : null] 443 | } 444 | } 445 | else { 446 | [queue: destination.toString()] 447 | } 448 | } 449 | } 450 | --------------------------------------------------------------------------------