From 6811bf945dcf2d779342f9afbf088f54c9739597 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sat, 17 Jul 2010 11:47:11 -0600 Subject: [PATCH 01/19] Added a base README for plugin documentation --- README | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 README diff --git a/README b/README new file mode 100644 index 0000000..fdf936b --- /dev/null +++ b/README @@ -0,0 +1,50 @@ +# Grails Cometd Plugin + +[CometD](http://cometd.org) is a scalable HTTP-based event routing bus that uses an Ajax Push technology pattern known as [Comet](http://en.wikipedia.org/wiki/Comet_(programming\)). This plugin allows your Grails application to push asynchronous notifications to HTTP clients using CometD and the [Bayeux](http://cometd.org/documentation/bayeux) protocol. + +## Installation +This didn't work for me, but: + + grails install-plugin cometd + +You can always download the [source](http://github.com/marcusb/grails-cometd) and then from the plugin directory: + + grails package-plugin + +And then from your application directory: + + grails install-plugin /path/to/grails-cometd-plugin-zip-file + +## Usage + +### CometD Servlet +*** + +The plugin configures a CometdServlet, mapped to the path cometd relative to your web application's context path. + +### Bayeux Service +*** + +A bean named bayeux is made available to your application. It is an instance of [BayeuxServer](http://download.cometd.org/bayeux-api-2.0.beta0-javadoc/org/cometd/bayeux/server/BayeuxServer.html). This is used to interact with the Comet server. + +### Configuration +*** + +The plugin is configured in Config.groovy, with options prefixed with "plugins.cometd". The following options are defined: + +* plugins.cometd.continuationFilter.disable: if set, do not install the to [ContinuationFilter](http://download.eclipse.org/jetty/stable-7/apidocs/org/eclipse/jetty/continuation/ContinuationFilter.html) + +## Contributing +Contributions are welcome, preferably by pull request on GitHub + + git clone git://github.com/marcusb/grails-cometd.git + +## History + +### Version 0.2.1 +*** +* Install the [ContinuationFilter](http://download.eclipse.org/jetty/stable-7/apidocs/org/eclipse/jetty/continuation/ContinuationFilter.html) by default, it is needed for Tomcat 6 + +### Version 0.2 +*** +* Rewritten from scratch for CometD 2.0 \ No newline at end of file From 4311d75f54afc10c522438badcfb9cf549e9bf39 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sat, 17 Jul 2010 11:51:49 -0600 Subject: [PATCH 02/19] Added markdown extension so that GitHub will display correctly --- README => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README => README.md (100%) diff --git a/README b/README.md similarity index 100% rename from README rename to README.md From 12dfc63840ffacb65cde7fc6ef4f7430d3c8c108 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sat, 17 Jul 2010 12:10:26 -0600 Subject: [PATCH 03/19] Upgraded the grails project to 1.3.2 --- application.properties | 8 ++++---- grails-app/conf/BuildConfig.groovy | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/application.properties b/application.properties index 7b1e902..96adabb 100644 --- a/application.properties +++ b/application.properties @@ -1,9 +1,9 @@ #Grails Metadata file -#Thu Jun 03 06:56:25 CEST 2010 -app.grails.version=1.3.1 +#Sat Jul 17 12:06:37 MDT 2010 +app.grails.version=1.3.2 app.name=grails-cometd app.servlet.version=2.4 app.version=0.1 plugins.functional-test=1.2.7 -plugins.hibernate=1.3.1 -plugins.tomcat=1.3.1 +plugins.hibernate=1.3.2 +plugins.tomcat=1.3.2 diff --git a/grails-app/conf/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy index fca161d..6c4d225 100644 --- a/grails-app/conf/BuildConfig.groovy +++ b/grails-app/conf/BuildConfig.groovy @@ -27,7 +27,7 @@ grails.project.dependency.resolution = { mavenCentral() } dependencies { - def cometdVer = '2.0.beta1' + def cometdVer = '2.0.0.RC3' compile(group: 'org.cometd.java', name: 'cometd-java-server', version: cometdVer) { excludes 'servlet-api' } From 22ed6f5911875c7e95ac1ab7b1dcf7e6228c807e Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sat, 17 Jul 2010 12:52:36 -0600 Subject: [PATCH 04/19] * Upgraded to Grails 1.3.3 * Updated README to reflect things that work now with 1.3.3 vs 1.3.2 --- README.md | 2 +- application.properties | 8 ++++---- grails-app/conf/BuildConfig.groovy | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fdf936b..8b60992 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [CometD](http://cometd.org) is a scalable HTTP-based event routing bus that uses an Ajax Push technology pattern known as [Comet](http://en.wikipedia.org/wiki/Comet_(programming\)). This plugin allows your Grails application to push asynchronous notifications to HTTP clients using CometD and the [Bayeux](http://cometd.org/documentation/bayeux) protocol. ## Installation -This didn't work for me, but: +NOTE: Because of a bug this won't work in grails 1.3.2 (fixed in 1.3.3): grails install-plugin cometd diff --git a/application.properties b/application.properties index 96adabb..5d62e9e 100644 --- a/application.properties +++ b/application.properties @@ -1,9 +1,9 @@ #Grails Metadata file -#Sat Jul 17 12:06:37 MDT 2010 -app.grails.version=1.3.2 +#Sat Jul 17 12:47:50 MDT 2010 +app.grails.version=1.3.3 app.name=grails-cometd app.servlet.version=2.4 app.version=0.1 plugins.functional-test=1.2.7 -plugins.hibernate=1.3.2 -plugins.tomcat=1.3.2 +plugins.hibernate=1.3.3 +plugins.tomcat=1.3.3 diff --git a/grails-app/conf/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy index 6c4d225..e066b0c 100644 --- a/grails-app/conf/BuildConfig.groovy +++ b/grails-app/conf/BuildConfig.groovy @@ -24,6 +24,7 @@ grails.project.dependency.resolution = { grailsPlugins() grailsHome() grailsCentral() + mavenLocal() mavenCentral() } dependencies { From fdd36aaa7769d98ffcef065167492a41370246af Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sun, 18 Jul 2010 00:09:25 -0600 Subject: [PATCH 05/19] Added ability to configure cometd through Config.groovy by supplying a map that will be used as init-params when the servlet is started. Probably the biggest use is to set the timeout for comet requests. --- CometdGrailsPlugin.groovy | 13 ++++++++++++- README.md | 6 +++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CometdGrailsPlugin.groovy b/CometdGrailsPlugin.groovy index c04fbb2..89ace1a 100644 --- a/CometdGrailsPlugin.groovy +++ b/CometdGrailsPlugin.groovy @@ -70,6 +70,17 @@ CometD and the Bayeux protocol. servlet { 'servlet-name'('cometd') 'servlet-class'(CometdServlet.class.name) + + // Add servlet init params from the config file + if (conf.init?.params) { + conf.init.params.each { key, value -> + 'init-param' { + 'param-name'(key) + 'param-value'(value) + } + println key + " :: " + value + } + } } } @@ -92,4 +103,4 @@ CometD and the Bayeux protocol. attributes = [(BayeuxServer.ATTRIBUTE): ref('bayeux')] } } -} +} \ No newline at end of file diff --git a/README.md b/README.md index 8b60992..7179acc 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,11 @@ A bean named bayeux is made available to your application. It is an instance of The plugin is configured in Config.groovy, with options prefixed with "plugins.cometd". The following options are defined: -* plugins.cometd.continuationFilter.disable: if set, do not install the to [ContinuationFilter](http://download.eclipse.org/jetty/stable-7/apidocs/org/eclipse/jetty/continuation/ContinuationFilter.html) +* **plugins.cometd.continuationFilter.disable**: if set, do not install the to [ContinuationFilter](http://download.eclipse.org/jetty/stable-7/apidocs/org/eclipse/jetty/continuation/ContinuationFilter.html) +* **plugins.cometd.init.params**: a map of init-name -> init-value pairs for an init-param element to be applied to the cometd servlet definition. Make sure you read the javadoc for org.cometd.bayeux.server.ServerTransport for how the init params are applied. Some useful settings include: + * **timeout**: In the default long polling http transport is the period of time the server waits before answering to a long poll + * **interval**: In the default long polling http transport is the period of time that the client waits between long polls + * **maxInterval**: In the default long polling http transport is the period of time that must elapse before the server consider the client being lost ## Contributing Contributions are welcome, preferably by pull request on GitHub From 6a4d8c68cdc0715090655a96ca6020523a9ca5cb Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sun, 18 Jul 2010 00:12:08 -0600 Subject: [PATCH 06/19] Removed logging statement --- CometdGrailsPlugin.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/CometdGrailsPlugin.groovy b/CometdGrailsPlugin.groovy index 89ace1a..081155e 100644 --- a/CometdGrailsPlugin.groovy +++ b/CometdGrailsPlugin.groovy @@ -78,7 +78,6 @@ CometD and the Bayeux protocol. 'param-name'(key) 'param-value'(value) } - println key + " :: " + value } } } From d3038e36ebce1f71894845dad7a05d037ffec774 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Wed, 21 Jul 2010 07:49:46 -0600 Subject: [PATCH 07/19] Implemented annotation based configuration for ChannelInitializers. Still need to refactor a bit. --- CometdGrailsPlugin.groovy | 27 +++++--- application.properties | 3 +- .../cometd/ServiceCometdProcessor.groovy | 58 +++++++++++++++++ .../plugin/cometd/ChannelInitializer.java | 8 +++ .../cometd/ServiceCometdProcessorSpec.groovy | 62 +++++++++++++++++++ 5 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy create mode 100644 src/java/grails/plugin/cometd/ChannelInitializer.java create mode 100644 test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy diff --git a/CometdGrailsPlugin.groovy b/CometdGrailsPlugin.groovy index 081155e..b236e30 100644 --- a/CometdGrailsPlugin.groovy +++ b/CometdGrailsPlugin.groovy @@ -21,6 +21,7 @@ import org.codehaus.groovy.grails.commons.ConfigurationHolder import org.cometd.server.BayeuxServerImpl import org.cometd.server.CometdServlet import org.cometd.bayeux.server.BayeuxServer +import grails.plugin.cometd.ServiceCometdProcessor import org.springframework.web.context.support.ServletContextAttributeExporter @@ -71,15 +72,15 @@ CometD and the Bayeux protocol. 'servlet-name'('cometd') 'servlet-class'(CometdServlet.class.name) - // Add servlet init params from the config file - if (conf.init?.params) { - conf.init.params.each { key, value -> - 'init-param' { - 'param-name'(key) - 'param-value'(value) - } - } - } + // Add servlet init params from the config file + if (conf.init?.params) { + conf.init.params.each { key, value -> + 'init-param' { + 'param-name'(key) + 'param-value'(value) + } + } + } } } @@ -102,4 +103,12 @@ CometD and the Bayeux protocol. attributes = [(BayeuxServer.ATTRIBUTE): ref('bayeux')] } } + + def doWithDynamicMethods = { context -> + def processor = new ServiceCometdProcessor() + + application.serviceClasses?.each { service -> + processor.process(service, context.bayeux) + } + } } \ No newline at end of file diff --git a/application.properties b/application.properties index 5d62e9e..761cdfb 100644 --- a/application.properties +++ b/application.properties @@ -1,9 +1,10 @@ #Grails Metadata file -#Sat Jul 17 12:47:50 MDT 2010 +#Mon Jul 19 23:14:01 MDT 2010 app.grails.version=1.3.3 app.name=grails-cometd app.servlet.version=2.4 app.version=0.1 plugins.functional-test=1.2.7 plugins.hibernate=1.3.3 +plugins.spock=0.4-groovy-1.7 plugins.tomcat=1.3.3 diff --git a/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy b/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy new file mode 100644 index 0000000..48b1c5b --- /dev/null +++ b/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy @@ -0,0 +1,58 @@ +package grails.plugin.cometd + +import org.codehaus.groovy.grails.commons.GrailsClassUtils as GCU +import org.cometd.bayeux.server.ConfigurableServerChannel + +class ServiceCometdProcessor { + final static EXPOSES = "exposes" + final static EXPOSE = "expose" + final static COMETD = "cometd" + + def configuration = [:] + + def process(service, bayeux) { + def clazz = service.clazz + + // If the service has declared itself a cometd service + if (ServiceCometdProcessor.exposesCometd(clazz)) { + // Process each method looking for cometd annotations + clazz.methods?.each { method -> + processServiceMethod(service, method, bayeux) + } + } + } + + /** + * Processes a method for cometd configuration annotations. + */ + def processServiceMethod(service, method, bayeux) { + processInitializer(service, method, bayeux) + } + + /** + * Tries to determine if the method is a channel initializer. + */ + def processInitializer(service, method, bayeux) { + def annotation = method.getAnnotation(ChannelInitializer) + + if (annotation) { + // Method must have one parameter. + if (method.parameterTypes.length != 1) { + throw new IllegalArgumentException("@ChannelInitializer: '${service.clazz.simpleName}.${method.name}' must declare one parameter") + } + + // TODO: Save off the config and do the initialization later + bayeux.createIfAbsent(annotation.value(), { channel -> + method.invoke(service, channel) + } as ConfigurableServerChannel.Initializer) + } + } + + /** + * Determine if the service class would like to register itself as a cometd service. It does so by declaring a staic variable "exposes" + * or "expose", which is an array that must contain "cometd" + */ + static exposesCometd(service) { + GCU.getStaticPropertyValue(service, EXPOSES)?.contains(COMETD) || GCU.getStaticPropertyValue(service, EXPOSE)?.contains(COMETD) + } +} \ No newline at end of file diff --git a/src/java/grails/plugin/cometd/ChannelInitializer.java b/src/java/grails/plugin/cometd/ChannelInitializer.java new file mode 100644 index 0000000..f9024f4 --- /dev/null +++ b/src/java/grails/plugin/cometd/ChannelInitializer.java @@ -0,0 +1,8 @@ +package grails.plugin.cometd; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +public @interface ChannelInitializer { + String value() default ""; +} \ No newline at end of file diff --git a/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy b/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy new file mode 100644 index 0000000..01b4cf4 --- /dev/null +++ b/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy @@ -0,0 +1,62 @@ +package grails.plugin.cometd + +import grails.plugin.spock.* + +import org.cometd.server.BayeuxServerImpl + +class ServiceCometdProcessorSpec extends UnitSpec { + def bayeux = new BayeuxServerImpl() + def processor = new ServiceCometdProcessor() + + def "can identify services that expose cometd functionality"() { + given: "a cometd service" + def service = CometdExposingService + + expect: "the processor to find that the service exposes cometd functionality" + ServiceCometdProcessor.exposesCometd(service) == true + } + + def "will ignore services that don't expose cometd functionality"() { + given: "a non-cometd service" + def service = NonCometdService + + expect: "the processor to find that the service doesn't expose cometd functionality" + ServiceCometdProcessor.exposesCometd(service) == false + } + + def "can find channel initializer method"() { + given: "a cometd service" + def service = new MethodInitializerService() + + when: "the service is processed" + processor.process(service, bayeux) + + then: "a channel should be created and it should be persistent" + def channel = bayeux.getChannel("/foo/bar") + channel != null + channel.persistent == true + } +} + +class BaseTestService { + def getClazz() { + return getClass() + } +} + +class CometdExposingService { + static exposes = ["jms", "xfire", "cometd"] +} + +class NonCometdService { + static exposes = ["jms"] +} + +class MethodInitializerService extends BaseTestService { + static exposes = ["cometd"] + + @ChannelInitializer("/foo/bar") + def configure(channel) { + channel.persistent = true + } +} \ No newline at end of file From 88f06775fa92cc6b794d6c3d4ba9f46bce9ce342 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sat, 24 Jul 2010 21:37:22 -0600 Subject: [PATCH 08/19] Implemented ability to define a message listener on a channel using a @MessageListener annotation. Reloading of services is in there, but is really not quite usable yet. --- CometdGrailsPlugin.groovy | 20 +- .../cometd/ServiceCometdProcessor.groovy | 212 +++++++++++++----- .../cometd/ServiceCometdProcessorSpec.groovy | 188 ++++++++++++---- 3 files changed, 318 insertions(+), 102 deletions(-) diff --git a/CometdGrailsPlugin.groovy b/CometdGrailsPlugin.groovy index b236e30..d0ef034 100644 --- a/CometdGrailsPlugin.groovy +++ b/CometdGrailsPlugin.groovy @@ -17,6 +17,7 @@ import grails.util.Environment import org.codehaus.groovy.grails.commons.ConfigurationHolder +import org.codehaus.groovy.grails.commons.ServiceArtefactHandler import org.cometd.server.BayeuxServerImpl import org.cometd.server.CometdServlet @@ -28,7 +29,6 @@ import org.springframework.web.context.support.ServletContextAttributeExporter class CometdGrailsPlugin { def version = "0.2.1" def grailsVersion = "1.3.1 > *" - def dependsOn = [:] def pluginExcludes = [ 'grails-app/services/**/test/', 'grails-app/views/error.gsp', @@ -45,6 +45,8 @@ This plugin allows your Grails application to send asynchronous notifications to CometD and the Bayeux protocol. ''' def documentation = "http://www.grails.org/plugin/cometd" + def loadAfter = ['services', 'controllers'] + def observe = ['services', 'controllers'] def doWithWebDescriptor = { xml -> def conf = ConfigurationHolder.config.plugins.cometd @@ -104,11 +106,19 @@ CometD and the Bayeux protocol. } } - def doWithDynamicMethods = { context -> - def processor = new ServiceCometdProcessor() - + def processor = new ServiceCometdProcessor() + + def doWithDynamicMethods = { context -> application.serviceClasses?.each { service -> - processor.process(service, context.bayeux) + processor.process(service.referenceInstance, context.bayeux) + } + } + + def onChange = { event -> + if (application.isServiceClass(event.source)) { + def artefact = application.addArtefact(ServiceArtefactHandler.TYPE, event.source) + + processor.process(artefact.referenceInstance, event.ctx.bayeux) } } } \ No newline at end of file diff --git a/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy b/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy index 48b1c5b..5a9f389 100644 --- a/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy +++ b/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy @@ -1,58 +1,168 @@ package grails.plugin.cometd import org.codehaus.groovy.grails.commons.GrailsClassUtils as GCU + +import org.cometd.bayeux.Message +import org.cometd.bayeux.server.ServerChannel import org.cometd.bayeux.server.ConfigurableServerChannel class ServiceCometdProcessor { - final static EXPOSES = "exposes" - final static EXPOSE = "expose" - final static COMETD = "cometd" - - def configuration = [:] - - def process(service, bayeux) { - def clazz = service.clazz - - // If the service has declared itself a cometd service - if (ServiceCometdProcessor.exposesCometd(clazz)) { - // Process each method looking for cometd annotations - clazz.methods?.each { method -> - processServiceMethod(service, method, bayeux) - } - } - } - - /** - * Processes a method for cometd configuration annotations. - */ - def processServiceMethod(service, method, bayeux) { - processInitializer(service, method, bayeux) - } - - /** - * Tries to determine if the method is a channel initializer. - */ - def processInitializer(service, method, bayeux) { - def annotation = method.getAnnotation(ChannelInitializer) - - if (annotation) { - // Method must have one parameter. - if (method.parameterTypes.length != 1) { - throw new IllegalArgumentException("@ChannelInitializer: '${service.clazz.simpleName}.${method.name}' must declare one parameter") - } - - // TODO: Save off the config and do the initialization later - bayeux.createIfAbsent(annotation.value(), { channel -> - method.invoke(service, channel) - } as ConfigurableServerChannel.Initializer) - } - } - - /** - * Determine if the service class would like to register itself as a cometd service. It does so by declaring a staic variable "exposes" - * or "expose", which is an array that must contain "cometd" - */ - static exposesCometd(service) { - GCU.getStaticPropertyValue(service, EXPOSES)?.contains(COMETD) || GCU.getStaticPropertyValue(service, EXPOSE)?.contains(COMETD) - } + final static EXPOSES = "exposes" + final static EXPOSE = "expose" + final static COMETD = "cometd" + + def configurations = [ + "initializers": [:], + "messageListeners": [:] + ] + + /** + * Determine if the service class would like to register itself as a cometd service. It does so by declaring a staic variable "exposes" + * or "expose", which is an array that must contain "cometd" + */ + static exposesCometd(service) { + GCU.getStaticPropertyValue(service, EXPOSES)?.contains(COMETD) || GCU.getStaticPropertyValue(service, EXPOSE)?.contains(COMETD) + } + + def process(service, bayeux) { + def clazz = service.class + + // If the service has declared itself a cometd service + if (ServiceCometdProcessor.exposesCometd(clazz)) { + // Setup a localsession for the service + def localSession = bayeux.newLocalSession(clazz.simpleName) + localSession.handshake(); + + clazz.metaClass.seeOwnPublishes = false + clazz.metaClass.localSession = localSession + clazz.metaClass.serverSession = localSession.serverSession + + // Process each method looking for cometd annotations + clazz.methods?.each { method -> + processServiceMethod(service, method) + } + + // Initialize all of the configurations. This allows us to control the order in which annotations are processed across many services + configurations.initializers.each { channel, configurations -> + configureInitializers(channel, configurations, bayeux) + } + + // Initialize all of the messageListeners + configurations.messageListeners.each { channel, configurations -> + configureMessageListeners(channel, configurations, bayeux) + } + } + } + + /** + * Processes a method for cometd configuration annotations. + */ + def processServiceMethod(service, method) { + processInitializer(service, method) + processMessageListener(service, method) + } + + /** + * Tries to determine if the method is a channel initializer. + */ + def processInitializer(service, method) { + def annotation = method.getAnnotation(ChannelInitializer) + + if (annotation) { + // Method must have one parameter. + if (method.parameterTypes.length != 1) { + throw new IllegalArgumentException(getParameterMessage(ChannelInitializer.class.simpleName, service, method, "one parameter")) + } + + // Add the method to the list of initializers for the given channel + addConfigurationForChannel("initializers", annotation.value(), annotation, service, method) + } + } + + /** + * Determines if the method is a message listener, and adds it to the configurations accordingly + */ + def processMessageListener(service, method) { + def annotation = method.getAnnotation(MessageListener) + + if (annotation) { + // Method must declare 2..4 parameters + if (!(2..4).contains(method.parameterTypes.length)) { + throw new IllegalArgumentException(getParameterMessage(MessageListener.class.simpleName, service, method, "2 to 4 parameters")) + } + + // Add the method to the list of message listeners for the channel + addConfigurationForChannel("messageListeners", annotation.value(), annotation, service, method) + } + } + + /** + * Each channel can have multiple initializers, possibly from multiple services. Here we initialize them all for a given channel. + */ + def configureInitializers(channel, configurations, bayeux) { + bayeux.createIfAbsent(channel, { configurableServerChannel -> + configurations.each { configuration -> + configuration.method.invoke(configuration.service, configurableServerChannel) + } + } as ConfigurableServerChannel.Initializer) + } + + def configureMessageListeners(channelId, configurations, bayeux) { + bayeux.createIfAbsent(channelId) + + configurations.each { configuration -> + def method = configuration.method + def service = configuration.service + def arguments = configuration.method.parameterTypes + + bayeux.getChannel(channelId).addListener({ session, channel, message -> + try { + if (service.seeOwnPublishes || session != service.serverSession) { + def data = Message.class.isAssignableFrom(arguments[1]) ? message : message.data + def reply + + switch (arguments.length) { + case 2: + reply = method.invoke(service, session, data) + break; + case 3: + reply = method.invoke(service, session, data, message.id) + break; + case 4: + reply = method.invoke(service, session, message.channel, data, message.id) + } + + if (reply instanceof Boolean && !reply){ + return false + } else { + session.deliver(service.serverSession, message.channel, reply, message.id); + } + } + } catch (e) { + e.printStackTrace(); + } + + return true + } as ServerChannel.MessageListener) + } + + } + + /** + * Helper method to add a configuration object to the configuration container for a given channel + */ + private addConfigurationForChannel(type, channel, annotation, service, method, ext) { + def channelConfigurations = configurations[type][channel] ?: [] + def configuration = [annotation: annotation, service: service, method: method] << ext + + configurations[type][channel] = channelConfigurations << configuration + } + + private addConfigurationForChannel(type, channel, annotation, service, method) { + addConfigurationForChannel(type, channel, annotation, service, method, [:]) + } + + private getParameterMessage(annotation, service, method, requirement) { + return "@${annotation}: '${service.class.simpleName}.${method.name}' declared ${method.parameterTypes.length} parameters, but must declare ${requirement}" + } } \ No newline at end of file diff --git a/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy b/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy index 01b4cf4..4b4ccb2 100644 --- a/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy +++ b/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy @@ -2,61 +2,157 @@ package grails.plugin.cometd import grails.plugin.spock.* +import org.cometd.bayeux.Message import org.cometd.server.BayeuxServerImpl class ServiceCometdProcessorSpec extends UnitSpec { - def bayeux = new BayeuxServerImpl() - def processor = new ServiceCometdProcessor() - - def "can identify services that expose cometd functionality"() { - given: "a cometd service" - def service = CometdExposingService - - expect: "the processor to find that the service exposes cometd functionality" - ServiceCometdProcessor.exposesCometd(service) == true - } - - def "will ignore services that don't expose cometd functionality"() { - given: "a non-cometd service" - def service = NonCometdService - - expect: "the processor to find that the service doesn't expose cometd functionality" - ServiceCometdProcessor.exposesCometd(service) == false - } - - def "can find channel initializer method"() { - given: "a cometd service" - def service = new MethodInitializerService() - - when: "the service is processed" - processor.process(service, bayeux) - - then: "a channel should be created and it should be persistent" - def channel = bayeux.getChannel("/foo/bar") - channel != null - channel.persistent == true - } -} - -class BaseTestService { - def getClazz() { - return getClass() - } + def bayeux = new BayeuxServerImpl() + def processor = new ServiceCometdProcessor() + + def "can identify services that expose cometd functionality"() { + given: "a cometd service" + def service = CometdExposingService + + expect: "the processor to find that the service exposes cometd functionality" + ServiceCometdProcessor.exposesCometd(service) == true + } + + def "will ignore services that don't expose cometd functionality"() { + given: "a non-cometd service" + def service = NonCometdService + + expect: "the processor to find that the service doesn't expose cometd functionality" + ServiceCometdProcessor.exposesCometd(service) == false + } + + def "can find channel initializer method"() { + given: "a cometd service which has a @ChannelInitializer annotated method" + def service = new MethodInitializerService() + + when: "the service is processed" + processor.process(service, bayeux) + + then: "a channel should be created and it should be persistent" + def channel = bayeux.getChannel("/foo/bar") + channel != null + channel.persistent == true + } + + def "encounters an exception when the annotated initializer method doesn't have the correct arguments"() { + given: "a cometd service which has a @ChannelInitializer annotated method with no arguments defined" + def service = new MethodInitializerExceptionService() + + when: "the service is processed" + processor.process(service, bayeux) + + then: "an exception should be thrown" + IllegalArgumentException iae = thrown() + } + + def "can register multiple initializers for one channel"() { + when: "the service is processed" + processor.process(service, bayeux) + + then: + bayeux.getChannel("/foo/bar").persistent == true + + where: + service << [new MethodInitializerService(), new MethodInitializerService()] + } + + def "can find message listener method and initialize it so that it is listening to channel publish events"() { + given: "a cometed service which has a @MessageListener annotated method" + def service = new MessageListenerService() + def local = bayeux.newLocalSession("local") + local.handshake() + + when: "the service is processed" + processor.process(service, bayeux) + bayeux.getChannel("/foo/bar").publish(local.serverSession, [body: "hola"], null) + + then: + bayeux.getChannel("/foo/bar") != null + service.body == "hola" + } + + def "can correctly initialize message listeners with different signatures"() { + given: "a cometed service which has @MessageListener annotated methods all with different method signatures" + def service = new MessageListenerSignaturesService() + def local = bayeux.newLocalSession("local") + local.handshake() + + when: "the service is processed and a message is published on the channel" + processor.process(service, bayeux) + bayeux.getChannel("/foo/bar").publish(local.serverSession, [body: "hola"], "17") + + then: + service.called.sort() == ["two", "twoTyped", "three", "four"].sort() + } } class CometdExposingService { - static exposes = ["jms", "xfire", "cometd"] + static exposes = ["jms", "xfire", "cometd"] } class NonCometdService { - static exposes = ["jms"] + static exposes = ["jms"] +} + +class MethodInitializerService { + static exposes = ["cometd"] + + @ChannelInitializer("/foo/bar") + def configure(channel) { + channel.persistent = true + } +} + +class MethodInitializerExceptionService { + static exposes = ["cometd"] + + @ChannelInitializer("/foo/bar") + def configure() {} +} + +class MessageListenerService { + static exposes = ["cometd"] + def body + + @MessageListener("/foo/bar") + def receive(session, message) { + body = message.body + } } -class MethodInitializerService extends BaseTestService { - static exposes = ["cometd"] - - @ChannelInitializer("/foo/bar") - def configure(channel) { - channel.persistent = true - } +class MessageListenerSignaturesService { + static exposes = ["cometd"] + def called = [] + + @MessageListener("/foo/bar") + def two(session, message) { + if (message.body == "hola") { + called << "two" + } + } + + @MessageListener("/foo/bar") + def twoTyped(session, Message message) { + if (message.data.body == "hola") { + called << "twoTyped" + } + } + + @MessageListener("/foo/bar") + def three(session, message, id) { + if (message.body == "hola" && id == "17") { + called << "three" + } + } + + @MessageListener("/foo/bar") + def four(session, channel, message, id) { + if (message.body == "hola" && id == "17" && channel == "/foo/bar") { + called << "four" + } + } } \ No newline at end of file From 91d36a7a3bc59b7819cf421318fdf8fd5a617cf1 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sat, 24 Jul 2010 21:39:15 -0600 Subject: [PATCH 09/19] Added @MessageListener annotation --- src/java/grails/plugin/cometd/MessageListener.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/java/grails/plugin/cometd/MessageListener.java diff --git a/src/java/grails/plugin/cometd/MessageListener.java b/src/java/grails/plugin/cometd/MessageListener.java new file mode 100644 index 0000000..0b65ec9 --- /dev/null +++ b/src/java/grails/plugin/cometd/MessageListener.java @@ -0,0 +1,8 @@ +package grails.plugin.cometd; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +public @interface MessageListener { + String value() default ""; +} \ No newline at end of file From 2d2f8feada29066f7df4fb098fe0c6964dd7b108 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sat, 24 Jul 2010 22:15:32 -0600 Subject: [PATCH 10/19] Updated documentation to reflect the latest changes. --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 7179acc..5132cec 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,34 @@ The plugin configures a CometdServlet, mapped to the path cometd relative to you A bean named bayeux is made available to your application. It is an instance of [BayeuxServer](http://download.cometd.org/bayeux-api-2.0.beta0-javadoc/org/cometd/bayeux/server/BayeuxServer.html). This is used to interact with the Comet server. +### Annotations +*** + +You can annotate your service classes and the plugin will wire everything up for you. This needs a bit of Cometd knowledge, but it's fairly straight forward. In order to register your service as a Cometd service you just have to add a static property to the service, much like the JMS and XFire plugins. (though this might be moving to a class level annotation, we'll see.) + + static exposes = ["cometd"] + +#### @ChannelInitializer +When you annotate a service method with the @ChannelInitializer annotation, the plugin will register it as a ConfigurableServerChannel.Initializer with the bayeux server. Your service method will be called with one argument, which is the ConfigurableServerChannel instance of the channel. Example: + + @ChannelInitializer("/foo/bar") + def configure(channel) { + channel.persistent = true + } + +#### @MessageListener +Annotating a service method with the @MessageListener annotation will register the method as a ServerChannel.MessageListener on the provided channel. The functionality is similar to that found with the AbstractService that comes with the Cometd distribution. Depending on the signature of your method, it will be called with different arguments. Possible options include: + + def service(session, data) // The "from" Session and Message.data + def service(session, data, id) // The "from" Session, Message.data, and the client id + def service(session, channel, message, id) // The "from" Session, the channel(String), the Message, and the client id + +If you were to explicitly define the parameter type for message you will get the actual Message object and not just the data field on the Message: + + def service(session, Message message) // The "from" Session and Message + +If you return false from the method, the message will not be broadcast. If you return nothing or true, the message will be broadcast. If you return an object or message, that message will be delivered to the client that initialized the service call. + ### Configuration *** From 507d34bbadaf2be5178b08f53cec32115a6176bb Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sat, 24 Jul 2010 22:20:08 -0600 Subject: [PATCH 11/19] Fixed some prior omissions in the documentation. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 5132cec..b9afa73 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,15 @@ If you were to explicitly define the parameter type for message you will get the If you return false from the method, the message will not be broadcast. If you return nothing or true, the message will be broadcast. If you return an object or message, that message will be delivered to the client that initialized the service call. +The actual use of this annotation would look thusly: + + @MessageListener("/foo/bar") + def onFooBar(session, data) { + // do something with the data + + [message: "thanks for the info mr. client"] + } + ### Configuration *** From 53e06c5f3a93bed40139b4741a0f09f79f6bee9d Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sun, 25 Jul 2010 10:33:22 -0600 Subject: [PATCH 12/19] Fixed a bug where cometd services were not exposing spring beans on the service when the service method was called. --- CometdGrailsPlugin.groovy | 2 +- .../plugins/cometd/test/EchoService.groovy | 41 ------------------- .../cometd/ServiceCometdProcessor.groovy | 19 +++++---- .../cometd/ServiceCometdProcessorSpec.groovy | 13 +++--- 4 files changed, 21 insertions(+), 54 deletions(-) delete mode 100644 grails-app/services/grails/plugins/cometd/test/EchoService.groovy diff --git a/CometdGrailsPlugin.groovy b/CometdGrailsPlugin.groovy index d0ef034..a303d61 100644 --- a/CometdGrailsPlugin.groovy +++ b/CometdGrailsPlugin.groovy @@ -110,7 +110,7 @@ CometD and the Bayeux protocol. def doWithDynamicMethods = { context -> application.serviceClasses?.each { service -> - processor.process(service.referenceInstance, context.bayeux) + processor.process(service.referenceInstance, context) } } diff --git a/grails-app/services/grails/plugins/cometd/test/EchoService.groovy b/grails-app/services/grails/plugins/cometd/test/EchoService.groovy deleted file mode 100644 index 7d7262e..0000000 --- a/grails-app/services/grails/plugins/cometd/test/EchoService.groovy +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © 2010 MBTE Sweden AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package grails.plugins.cometd.test - -import org.cometd.bayeux.server.ServerChannel - -import org.springframework.beans.factory.InitializingBean - -class EchoService implements InitializingBean -{ - def bayeux - - void afterPropertiesSet() - { - assert bayeux : 'bayeux object must be set' - def localSession = bayeux.newLocalSession('echo') - localSession.handshake() - def serverSession = localSession.getServerSession() - def channel = bayeux.getChannel('/echo', true) - channel.addListener({ from, chan, msg -> - if (from != localSession.getServerSession()) { - from.deliver serverSession, chan.id, [echo: msg.data.msg], null - } - true - } as ServerChannel.MessageListener) - } -} diff --git a/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy b/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy index 5a9f389..1b7c1f2 100644 --- a/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy +++ b/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy @@ -1,5 +1,6 @@ package grails.plugin.cometd +import grails.util.GrailsNameUtils as GNU import org.codehaus.groovy.grails.commons.GrailsClassUtils as GCU import org.cometd.bayeux.Message @@ -11,7 +12,8 @@ class ServiceCometdProcessor { final static EXPOSE = "expose" final static COMETD = "cometd" - def configurations = [ + def _context + def _configurations = [ "initializers": [:], "messageListeners": [:] ] @@ -24,8 +26,11 @@ class ServiceCometdProcessor { GCU.getStaticPropertyValue(service, EXPOSES)?.contains(COMETD) || GCU.getStaticPropertyValue(service, EXPOSE)?.contains(COMETD) } - def process(service, bayeux) { + def process(service, context) { def clazz = service.class + def bayeux = context.bayeux + + _context = context; // If the service has declared itself a cometd service if (ServiceCometdProcessor.exposesCometd(clazz)) { @@ -43,12 +48,12 @@ class ServiceCometdProcessor { } // Initialize all of the configurations. This allows us to control the order in which annotations are processed across many services - configurations.initializers.each { channel, configurations -> + _configurations.initializers.each { channel, configurations -> configureInitializers(channel, configurations, bayeux) } // Initialize all of the messageListeners - configurations.messageListeners.each { channel, configurations -> + _configurations.messageListeners.each { channel, configurations -> configureMessageListeners(channel, configurations, bayeux) } } @@ -112,7 +117,7 @@ class ServiceCometdProcessor { configurations.each { configuration -> def method = configuration.method - def service = configuration.service + def service = _context[GNU.getPropertyNameRepresentation(configuration.service.class)] def arguments = configuration.method.parameterTypes bayeux.getChannel(channelId).addListener({ session, channel, message -> @@ -152,10 +157,10 @@ class ServiceCometdProcessor { * Helper method to add a configuration object to the configuration container for a given channel */ private addConfigurationForChannel(type, channel, annotation, service, method, ext) { - def channelConfigurations = configurations[type][channel] ?: [] + def channelConfigurations = _configurations[type][channel] ?: [] def configuration = [annotation: annotation, service: service, method: method] << ext - configurations[type][channel] = channelConfigurations << configuration + _configurations[type][channel] = channelConfigurations << configuration } private addConfigurationForChannel(type, channel, annotation, service, method) { diff --git a/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy b/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy index 4b4ccb2..9fc4c2d 100644 --- a/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy +++ b/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy @@ -8,6 +8,7 @@ import org.cometd.server.BayeuxServerImpl class ServiceCometdProcessorSpec extends UnitSpec { def bayeux = new BayeuxServerImpl() def processor = new ServiceCometdProcessor() + def context = [bayeux: bayeux] def "can identify services that expose cometd functionality"() { given: "a cometd service" @@ -30,7 +31,7 @@ class ServiceCometdProcessorSpec extends UnitSpec { def service = new MethodInitializerService() when: "the service is processed" - processor.process(service, bayeux) + processor.process(service, context) then: "a channel should be created and it should be persistent" def channel = bayeux.getChannel("/foo/bar") @@ -43,7 +44,7 @@ class ServiceCometdProcessorSpec extends UnitSpec { def service = new MethodInitializerExceptionService() when: "the service is processed" - processor.process(service, bayeux) + processor.process(service, context) then: "an exception should be thrown" IllegalArgumentException iae = thrown() @@ -51,7 +52,7 @@ class ServiceCometdProcessorSpec extends UnitSpec { def "can register multiple initializers for one channel"() { when: "the service is processed" - processor.process(service, bayeux) + processor.process(service, context) then: bayeux.getChannel("/foo/bar").persistent == true @@ -63,11 +64,12 @@ class ServiceCometdProcessorSpec extends UnitSpec { def "can find message listener method and initialize it so that it is listening to channel publish events"() { given: "a cometed service which has a @MessageListener annotated method" def service = new MessageListenerService() + context["messageListenerService"] = service def local = bayeux.newLocalSession("local") local.handshake() when: "the service is processed" - processor.process(service, bayeux) + processor.process(service, context) bayeux.getChannel("/foo/bar").publish(local.serverSession, [body: "hola"], null) then: @@ -78,11 +80,12 @@ class ServiceCometdProcessorSpec extends UnitSpec { def "can correctly initialize message listeners with different signatures"() { given: "a cometed service which has @MessageListener annotated methods all with different method signatures" def service = new MessageListenerSignaturesService() + context["messageListenerSignaturesService"] = service def local = bayeux.newLocalSession("local") local.handshake() when: "the service is processed and a message is published on the channel" - processor.process(service, bayeux) + processor.process(service, context) bayeux.getChannel("/foo/bar").publish(local.serverSession, [body: "hola"], "17") then: From 7741abbc8a488d1ed29f6252739ec42a3ffe243f Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Mon, 26 Jul 2010 22:35:42 -0600 Subject: [PATCH 13/19] Implemented reloading of message listeners. We might still run into reloading issues, but right now it's working pretty smoothly. --- CometdGrailsPlugin.groovy | 10 +- .../cometd/ServiceCometdProcessor.groovy | 92 +++++++++---------- .../cometd/ServiceCometdProcessorSpec.groovy | 31 +++++-- 3 files changed, 78 insertions(+), 55 deletions(-) diff --git a/CometdGrailsPlugin.groovy b/CometdGrailsPlugin.groovy index a303d61..4947ada 100644 --- a/CometdGrailsPlugin.groovy +++ b/CometdGrailsPlugin.groovy @@ -26,7 +26,12 @@ import grails.plugin.cometd.ServiceCometdProcessor import org.springframework.web.context.support.ServletContextAttributeExporter +import org.apache.commons.logging.LogFactory + class CometdGrailsPlugin { + + static LOG = LogFactory.getLog('grails.plugin.cometd.CometdGrailsPlugin') + def version = "0.2.1" def grailsVersion = "1.3.1 > *" def pluginExcludes = [ @@ -50,6 +55,8 @@ CometD and the Bayeux protocol. def doWithWebDescriptor = { xml -> def conf = ConfigurationHolder.config.plugins.cometd + + LOG.debug("Initializing with continutationFilter: ${!conf.continuationFilter.disable}") if (!conf.continuationFilter.disable) { def filters = xml.'filter' filters[filters.size() - 1] + { @@ -76,6 +83,7 @@ CometD and the Bayeux protocol. // Add servlet init params from the config file if (conf.init?.params) { + LOG.debug("Initializing with init-params: ${conf.init.params}") conf.init.params.each { key, value -> 'init-param' { 'param-name'(key) @@ -118,7 +126,7 @@ CometD and the Bayeux protocol. if (application.isServiceClass(event.source)) { def artefact = application.addArtefact(ServiceArtefactHandler.TYPE, event.source) - processor.process(artefact.referenceInstance, event.ctx.bayeux) + processor.process(artefact.referenceInstance, event.ctx) } } } \ No newline at end of file diff --git a/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy b/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy index 1b7c1f2..2c4b978 100644 --- a/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy +++ b/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy @@ -12,7 +12,8 @@ class ServiceCometdProcessor { final static EXPOSE = "expose" final static COMETD = "cometd" - def _context + def _context + def _messageListenerRemovers = [:] def _configurations = [ "initializers": [:], "messageListeners": [:] @@ -28,9 +29,9 @@ class ServiceCometdProcessor { def process(service, context) { def clazz = service.class - def bayeux = context.bayeux - - _context = context; + def bayeux = context.bayeux + + _context = context; // If the service has declared itself a cometd service if (ServiceCometdProcessor.exposesCometd(clazz)) { @@ -51,6 +52,9 @@ class ServiceCometdProcessor { _configurations.initializers.each { channel, configurations -> configureInitializers(channel, configurations, bayeux) } + + // If listeners for this class exist, we need to remove them + _messageListenerRemovers.get(clazz.name, []).each({remover -> remover.call()}).clear() // Initialize all of the messageListeners _configurations.messageListeners.each { channel, configurations -> @@ -80,7 +84,7 @@ class ServiceCometdProcessor { } // Add the method to the list of initializers for the given channel - addConfigurationForChannel("initializers", annotation.value(), annotation, service, method) + _configurations["initializers"].get(annotation.value(), []) << [annotation: annotation, service: service, method: method] } } @@ -97,7 +101,7 @@ class ServiceCometdProcessor { } // Add the method to the list of message listeners for the channel - addConfigurationForChannel("messageListeners", annotation.value(), annotation, service, method) + _configurations["messageListeners"].get(annotation.value(), []) << [annotation: annotation, service: service, method: method] } } @@ -117,56 +121,52 @@ class ServiceCometdProcessor { configurations.each { configuration -> def method = configuration.method - def service = _context[GNU.getPropertyNameRepresentation(configuration.service.class)] + def serviceClass = configuration.service.class + def service = _context[GNU.getPropertyNameRepresentation(serviceClass)] def arguments = configuration.method.parameterTypes + def channel = bayeux.getChannel(channelId) + + def listener = { session, listenerChannel, message -> + try { + if (service.seeOwnPublishes || session != service.serverSession) { + def data = Message.class.isAssignableFrom(arguments[1]) ? message : message.data + def reply + + switch (arguments.length) { + case 2: + reply = method.invoke(service, session, data) + break; + case 3: + reply = method.invoke(service, session, data, message.id) + break; + case 4: + reply = method.invoke(service, session, message.channel, data, message.id) + } - bayeux.getChannel(channelId).addListener({ session, channel, message -> - try { - if (service.seeOwnPublishes || session != service.serverSession) { - def data = Message.class.isAssignableFrom(arguments[1]) ? message : message.data - def reply - - switch (arguments.length) { - case 2: - reply = method.invoke(service, session, data) - break; - case 3: - reply = method.invoke(service, session, data, message.id) - break; - case 4: - reply = method.invoke(service, session, message.channel, data, message.id) - } - - if (reply instanceof Boolean && !reply){ - return false - } else { - session.deliver(service.serverSession, message.channel, reply, message.id); - } + if (reply instanceof Boolean && !reply){ + return false + } else { + session.deliver(service.serverSession, message.channel, reply, message.id); } - } catch (e) { - e.printStackTrace(); } + } catch (e) { + e.printStackTrace(); + } return true - } as ServerChannel.MessageListener) + } as ServerChannel.MessageListener + + // Add a remover closure, so that we can easily detatch the listener and ditch the configuration + _messageListenerRemovers.get(serviceClass.name, []) << { + channel.removeListener(listener) + configurations.remove(configuration) + } + + channel.addListener(listener) } } - /** - * Helper method to add a configuration object to the configuration container for a given channel - */ - private addConfigurationForChannel(type, channel, annotation, service, method, ext) { - def channelConfigurations = _configurations[type][channel] ?: [] - def configuration = [annotation: annotation, service: service, method: method] << ext - - _configurations[type][channel] = channelConfigurations << configuration - } - - private addConfigurationForChannel(type, channel, annotation, service, method) { - addConfigurationForChannel(type, channel, annotation, service, method, [:]) - } - private getParameterMessage(annotation, service, method, requirement) { return "@${annotation}: '${service.class.simpleName}.${method.name}' declared ${method.parameterTypes.length} parameters, but must declare ${requirement}" } diff --git a/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy b/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy index 9fc4c2d..a03f578 100644 --- a/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy +++ b/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy @@ -8,7 +8,7 @@ import org.cometd.server.BayeuxServerImpl class ServiceCometdProcessorSpec extends UnitSpec { def bayeux = new BayeuxServerImpl() def processor = new ServiceCometdProcessor() - def context = [bayeux: bayeux] + def context = [bayeux: bayeux] def "can identify services that expose cometd functionality"() { given: "a cometd service" @@ -54,25 +54,25 @@ class ServiceCometdProcessorSpec extends UnitSpec { when: "the service is processed" processor.process(service, context) - then: + then: "the channel should be initialized" bayeux.getChannel("/foo/bar").persistent == true - where: + where: "there are two services" service << [new MethodInitializerService(), new MethodInitializerService()] } def "can find message listener method and initialize it so that it is listening to channel publish events"() { given: "a cometed service which has a @MessageListener annotated method" def service = new MessageListenerService() - context["messageListenerService"] = service + context["messageListenerService"] = service def local = bayeux.newLocalSession("local") local.handshake() - when: "the service is processed" + when: "the service is processed and the channel is published to" processor.process(service, context) bayeux.getChannel("/foo/bar").publish(local.serverSession, [body: "hola"], null) - then: + then: "the method listener should be registered and called" bayeux.getChannel("/foo/bar") != null service.body == "hola" } @@ -80,7 +80,7 @@ class ServiceCometdProcessorSpec extends UnitSpec { def "can correctly initialize message listeners with different signatures"() { given: "a cometed service which has @MessageListener annotated methods all with different method signatures" def service = new MessageListenerSignaturesService() - context["messageListenerSignaturesService"] = service + context["messageListenerSignaturesService"] = service def local = bayeux.newLocalSession("local") local.handshake() @@ -88,9 +88,24 @@ class ServiceCometdProcessorSpec extends UnitSpec { processor.process(service, context) bayeux.getChannel("/foo/bar").publish(local.serverSession, [body: "hola"], "17") - then: + then: "all listeners should be initialized and called" service.called.sort() == ["two", "twoTyped", "three", "four"].sort() } + + def "can reload message listeners gracefully"() { + given: "a cometed service with @MessageListeners" + def service = new MessageListenerService() + context["messageListenerService"] = service + def local = bayeux.newLocalSession("local") + local.handshake() + + when: "the service is processed and then reprocessed" + processor.process(service, context) + processor.process(service, context) + + then: "the channel should have the correct listeners attached" + bayeux.getChannel("/foo/bar").listeners.size() == 1 + } } class CometdExposingService { From 0de511c2c2fff08fc3b74bb64c0ed511352029e3 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Wed, 28 Jul 2010 22:28:33 -0600 Subject: [PATCH 14/19] Added ability to supress broadcasting from a message listener using the annotation --- .../grails/plugin/cometd/ServiceCometdProcessor.groovy | 10 ++++------ src/java/grails/plugin/cometd/MessageListener.java | 2 ++ .../plugin/cometd/ServiceCometdProcessorSpec.groovy | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy b/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy index 2c4b978..7de55ba 100644 --- a/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy +++ b/src/groovy/grails/plugin/cometd/ServiceCometdProcessor.groovy @@ -101,7 +101,7 @@ class ServiceCometdProcessor { } // Add the method to the list of message listeners for the channel - _configurations["messageListeners"].get(annotation.value(), []) << [annotation: annotation, service: service, method: method] + _configurations["messageListeners"].get(annotation.value() ?: annotation.channel(), []) << [annotation: annotation, service: service, method: method] } } @@ -142,10 +142,8 @@ class ServiceCometdProcessor { case 4: reply = method.invoke(service, session, message.channel, data, message.id) } - - if (reply instanceof Boolean && !reply){ - return false - } else { + + if (reply){ session.deliver(service.serverSession, message.channel, reply, message.id); } } @@ -153,7 +151,7 @@ class ServiceCometdProcessor { e.printStackTrace(); } - return true + return configuration.annotation.broadcast() } as ServerChannel.MessageListener // Add a remover closure, so that we can easily detatch the listener and ditch the configuration diff --git a/src/java/grails/plugin/cometd/MessageListener.java b/src/java/grails/plugin/cometd/MessageListener.java index 0b65ec9..fb6278f 100644 --- a/src/java/grails/plugin/cometd/MessageListener.java +++ b/src/java/grails/plugin/cometd/MessageListener.java @@ -5,4 +5,6 @@ @Retention(RetentionPolicy.RUNTIME) public @interface MessageListener { String value() default ""; + String channel() default ""; + boolean broadcast() default true; } \ No newline at end of file diff --git a/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy b/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy index a03f578..02c644e 100644 --- a/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy +++ b/test/unit/grails/plugin/cometd/ServiceCometdProcessorSpec.groovy @@ -136,7 +136,7 @@ class MessageListenerService { static exposes = ["cometd"] def body - @MessageListener("/foo/bar") + @MessageListener(channel = "/foo/bar", broadcast = false) def receive(session, message) { body = message.body } From a10684baf02174f9e507a14f88d3868e48925102 Mon Sep 17 00:00:00 2001 From: Danilo Tuler Date: Thu, 17 Mar 2011 00:27:03 -0300 Subject: [PATCH 15/19] upgrading cometd from 2.0.0 to 2.1.0 --- CometdGrailsPlugin.groovy | 2 +- application.properties | 8 ++++---- grails-app/conf/BuildConfig.groovy | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CometdGrailsPlugin.groovy b/CometdGrailsPlugin.groovy index 3f82d3a..3f68fb3 100644 --- a/CometdGrailsPlugin.groovy +++ b/CometdGrailsPlugin.groovy @@ -25,7 +25,7 @@ import org.cometd.bayeux.server.BayeuxServer import org.springframework.web.context.support.ServletContextAttributeExporter class CometdGrailsPlugin { - def version = "0.2.2" + def version = "0.2.3" def grailsVersion = "1.2.1 > *" def dependsOn = [:] def pluginExcludes = [ diff --git a/application.properties b/application.properties index 021ea07..0fb1f2c 100644 --- a/application.properties +++ b/application.properties @@ -1,9 +1,9 @@ #Grails Metadata file -#Tue Oct 19 04:56:43 GMT+01:00 2010 -app.grails.version=1.3.5 +#Wed Mar 16 16:16:40 BRT 2011 +app.grails.version=1.3.6 app.name=grails-cometd app.servlet.version=2.4 app.version=0.1 plugins.functional-test=1.2.7 -plugins.hibernate=1.3.5 -plugins.tomcat=1.3.5 +plugins.hibernate=1.3.6 +plugins.tomcat=1.3.6 diff --git a/grails-app/conf/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy index 84be2a4..ef5b5c0 100644 --- a/grails-app/conf/BuildConfig.groovy +++ b/grails-app/conf/BuildConfig.groovy @@ -24,7 +24,7 @@ grails.project.dependency.resolution = { mavenCentral() } dependencies { - def cometdVer = '2.0.0' + def cometdVer = '2.1.0' compile(group: 'org.cometd.java', name: 'cometd-java-server', version: cometdVer) { excludes 'servlet-api' } From fbb7d5ecaa86bf22aac6f422b080ab18f49c901b Mon Sep 17 00:00:00 2001 From: Danilo Tuler Date: Thu, 14 Apr 2011 14:27:15 -0300 Subject: [PATCH 16/19] upgrading to cometd 2.1.1 and grails 1.3.7 --- CometdGrailsPlugin.groovy | 2 +- application.properties | 8 ++++---- grails-app/conf/BuildConfig.groovy | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CometdGrailsPlugin.groovy b/CometdGrailsPlugin.groovy index 3f68fb3..70c17fb 100644 --- a/CometdGrailsPlugin.groovy +++ b/CometdGrailsPlugin.groovy @@ -25,7 +25,7 @@ import org.cometd.bayeux.server.BayeuxServer import org.springframework.web.context.support.ServletContextAttributeExporter class CometdGrailsPlugin { - def version = "0.2.3" + def version = "0.2.4" def grailsVersion = "1.2.1 > *" def dependsOn = [:] def pluginExcludes = [ diff --git a/application.properties b/application.properties index 0fb1f2c..9288920 100644 --- a/application.properties +++ b/application.properties @@ -1,9 +1,9 @@ #Grails Metadata file -#Wed Mar 16 16:16:40 BRT 2011 -app.grails.version=1.3.6 +#Thu Apr 14 14:23:13 BRT 2011 +app.grails.version=1.3.7 app.name=grails-cometd app.servlet.version=2.4 app.version=0.1 plugins.functional-test=1.2.7 -plugins.hibernate=1.3.6 -plugins.tomcat=1.3.6 +plugins.hibernate=1.3.7 +plugins.tomcat=1.3.7 diff --git a/grails-app/conf/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy index ef5b5c0..c70a7d6 100644 --- a/grails-app/conf/BuildConfig.groovy +++ b/grails-app/conf/BuildConfig.groovy @@ -24,7 +24,7 @@ grails.project.dependency.resolution = { mavenCentral() } dependencies { - def cometdVer = '2.1.0' + def cometdVer = '2.1.1' compile(group: 'org.cometd.java', name: 'cometd-java-server', version: cometdVer) { excludes 'servlet-api' } From dc3ad12ed42ab7ec2b7c569081120273b0cda3ef Mon Sep 17 00:00:00 2001 From: Igor Aguiar Date: Mon, 27 Jun 2011 17:24:48 -0300 Subject: [PATCH 17/19] Upgrading cometd from 2.1.1 to 2.2.0 --- grails-app/conf/BuildConfig.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grails-app/conf/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy index 8036e71..f3594fd 100644 --- a/grails-app/conf/BuildConfig.groovy +++ b/grails-app/conf/BuildConfig.groovy @@ -28,7 +28,7 @@ grails.project.dependency.resolution = { mavenCentral() } dependencies { - def cometdVer = '2.1.1' + def cometdVer = '2.2.0' compile(group: 'org.cometd.java', name: 'cometd-java-server', version: cometdVer) { excludes 'servlet-api' } From 310629df474e087fa059fad0b4a1621494227d80 Mon Sep 17 00:00:00 2001 From: Igor Aguiar Date: Mon, 27 Jun 2011 17:25:30 -0300 Subject: [PATCH 18/19] Fixes BayeuxServerImpl constructor call --- CometdGrailsPlugin.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CometdGrailsPlugin.groovy b/CometdGrailsPlugin.groovy index 6c941ec..a50acb0 100644 --- a/CometdGrailsPlugin.groovy +++ b/CometdGrailsPlugin.groovy @@ -102,7 +102,7 @@ CometD and the Bayeux protocol. } def doWithSpring = { - bayeux(BayeuxServerImpl, true) { bean -> + bayeux(BayeuxServerImpl) { bean -> bean.destroyMethod = 'stop' } From 917c3d39b45508533759aaeaaa610795c08d87e8 Mon Sep 17 00:00:00 2001 From: Igor Aguiar Date: Mon, 27 Jun 2011 17:26:07 -0300 Subject: [PATCH 19/19] Changes plugin version from 0.2.4 to 0.2.5 --- CometdGrailsPlugin.groovy | 4 ++-- application.properties | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CometdGrailsPlugin.groovy b/CometdGrailsPlugin.groovy index a50acb0..915af54 100644 --- a/CometdGrailsPlugin.groovy +++ b/CometdGrailsPlugin.groovy @@ -29,7 +29,7 @@ import org.springframework.web.context.support.ServletContextAttributeExporter import org.apache.commons.logging.LogFactory class CometdGrailsPlugin { - def version = "0.2.4" + def version = "0.2.5" def grailsVersion = "1.2.1 > *" def dependsOn = [:] def pluginExcludes = [ @@ -127,4 +127,4 @@ CometD and the Bayeux protocol. processor.process(artefact.referenceInstance, event.ctx) } } -} \ No newline at end of file +} diff --git a/application.properties b/application.properties index fedcd86..9288920 100644 --- a/application.properties +++ b/application.properties @@ -7,4 +7,3 @@ app.version=0.1 plugins.functional-test=1.2.7 plugins.hibernate=1.3.7 plugins.tomcat=1.3.7 -plugins.spock=0.4-groovy-1.7 \ No newline at end of file