Permalink
Browse files

changes to make it backward compatible with the plugin

  • Loading branch information...
1 parent add922d commit cf4d0dce32d9fd1938895c49847321a1db666112 Joshua Burnett committed Dec 31, 2011
Showing with 2,565 additions and 109 deletions.
  1. +198 −23 Quartz2GrailsPlugin.groovy
  2. +52 −11 README.md
  3. +2 −2 grails-app/conf/Quartz2DefaultConfig.groovy
  4. +266 −22 quartz2.tmproj
  5. +59 −0 scripts/CreateJob.groovy
  6. +25 −10 scripts/_Install.groovy
  7. +26 −10 scripts/_Upgrade.groovy
  8. +91 −0 src/groovy/grails/plugin/quartz2/GrailsArtefactJobDetailFactoryBean.groovy
  9. +26 −0 src/groovy/grails/plugin/quartz2/MergedConfigHolder.groovy
  10. +2 −2 src/groovy/grails/plugin/quartz2/PersistenceContextJobListener.groovy
  11. +65 −23 src/groovy/grails/plugin/quartz2/QuartzFactoryBean.groovy
  12. +82 −0 src/groovy/grails/plugin/quartz2/SpringBeanJob.groovy
  13. +93 −0 src/groovy/grails/plugin/quartz2/TriggerHelper.groovy
  14. +189 −0 src/groovy/grails/plugin/quartz2/TriggersBuilder.groovy
  15. +117 −0 src/java/grails/plugin/quartz2/DefaultGrailsJobClass.java
  16. +86 −0 src/java/grails/plugin/quartz2/GrailsArtefactJob.java
  17. +85 −0 src/java/grails/plugin/quartz2/GrailsJobClass.java
  18. +67 −0 src/java/grails/plugin/quartz2/GrailsJobClassConstants.java
  19. +56 −0 src/java/grails/plugin/quartz2/GrailsJobFactory.java
  20. +55 −0 src/java/grails/plugin/quartz2/JobArtefactHandler.java
  21. +155 −0 src/java/grails/plugin/quartz2/LocalDataSourceJobStore.java
  22. +11 −0 src/templates/artifacts/Job.groovy
  23. +7 −2 test/projects/qkiss/app-qkiss-quartz.groovy
  24. +18 −0 test/projects/qkiss/grails-app/jobs/qkiss/ConfigTriggerJob.groovy
  25. +15 −0 test/projects/qkiss/grails-app/jobs/qkiss/CronJob.groovy
  26. +21 −0 test/projects/qkiss/grails-app/jobs/qkiss/CronNonConcurrentJob.groovy
  27. +17 −0 test/projects/qkiss/grails-app/jobs/qkiss/SimpleJob.groovy
  28. +15 −0 test/projects/qkiss/grails-app/services/qkiss/SpringBeanTestService.groovy
  29. +1 −3 test/projects/qkiss/src/groovy/qkiss/HelloFromExternalConfigJob.groovy
  30. +52 −0 test/projects/qkiss/test/integration/qkiss/ArtifactTests.groovy
  31. +1 −1 test/projects/qkiss/test/integration/qkiss/BuilderConfigTests.groovy
  32. +69 −0 test/projects/qkiss/test/integration/qkiss/QuartzPluginTests.groovy
  33. +39 −0 test/projects/qkiss/test/integration/qkiss/SpringBeanJobTests.groovy
  34. +137 −0 test/unit/grails/plugin/quartz2/DefaultGrailsJobClassTests.groovy
  35. +55 −0 test/unit/grails/plugin/quartz2/JobArtefactHandlerTests.groovy
  36. +51 −0 test/unit/grails/plugin/quartz2/MockDoWithSpring.groovy
  37. +79 −0 test/unit/grails/plugin/quartz2/TriggersBuilderCreateTests.groovy
  38. +180 −0 test/unit/grails/plugin/quartz2/TriggersBuilderTests.groovy
@@ -1,10 +1,29 @@
+/*
+ * Copyright (c) 2011 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
import grails.plugin.quartz2.*
+import org.quartz.*
+import org.springframework.context.ApplicationContext
+import org.springframework.beans.factory.config.MethodInvokingFactoryBean
class Quartz2GrailsPlugin {
// the plugin version
- def version = "0.2.1.1"
+ def version = "0.2.1.2"
// the version or versions of Grails the plugin is designed for
- def grailsVersion = "1.3.1 > *"
+ def grailsVersion = "1.3 > *"
// the other plugins this plugin depends on
def dependsOn = [:]
// resources that are excluded from plugin packaging
@@ -15,35 +34,51 @@ class Quartz2GrailsPlugin {
// TODO Fill in these fields
def author = "Joshua Burentt"
def authorEmail = "Joshua@greenbill.com"
- def title = "Quartz 2.1 Scheduler"
+ def title = "Quartz 2.x Scheduler"
def description = '''\\
-Uses the new Quartz 2.1 framework from quartz-scheduler.org.
+Uses the new Quartz 2 framework from quartz-scheduler.org.
The goal is to keep it as simple as possible while making it friendly for Groovy/Grails.
'''
// URL to the plugin's documentation
def documentation = "http://grails.org/plugin/quartz2"
- def doWithWebDescriptor = { xml ->
- // TODO Implement additions to web.xml (optional), this event occurs before
- }
+ def watchedResources = [
+ "file:./grails-app/jobs/**/*Job.groovy",
+ "file:./plugins/*/grails-app/jobs/**/*Job.groovy"
+ ]
+
+ def artefacts = [new JobArtefactHandler()]
def doWithSpring = {
def mcfg = application.mergedConfig
+ MergedConfigHolder.config = application.mergedConfig
+
def quartzProps = loadQuartzConfig(mcfg)
+ application.jobClasses.each {jobClass ->
+ configureJobBeans.delegate = delegate
+ configureJobBeans(jobClass)
+ }
+
persistenceContextJobListener(PersistenceContextJobListener){
persistenceInterceptor = ref("persistenceInterceptor")
}
jobErrorLoggerListener(JobErrorLoggerListener)
-
+ quartzJobFactory(GrailsJobFactory)
+
quartzScheduler(QuartzFactoryBean) {
grailsApplication = ref('grailsApplication')
quartzProperties = quartzProps
+ jobFactory = quartzJobFactory
// delay scheduler startup to after-bootstrap stage
- autoStartup = mcfg.grails.plugins.quartz2.autoStartup
+ autoStartup = mcfg.grails.plugin.quartz2.autoStartup
globalJobListeners = [ref('jobErrorLoggerListener'),ref('persistenceContextJobListener')]
+ if (mcfg.grails.plugin.quartz2.jdbcStore) {
+ dataSource = ref('dataSource')
+ transactionManager = ref('transactionManager')
+ }
}
/* for future reloading
@@ -60,37 +95,177 @@ The goal is to keep it as simple as possible while making it friendly for Groovy
configReloadingTask(ConfigReloadingTask)
*/
-
}
- def doWithDynamicMethods = { ctx ->
- // TODO Implement registering dynamic methods to classes (optional)
+ def doWithDynamicMethods = {ctx ->
+ def random = new Random()
+ Scheduler quartzScheduler = ctx.getBean('quartzScheduler')
+ application.jobClasses.each {GrailsJobClass tc ->
+ def mc = tc.metaClass
+ //println "** doWithDynamicMethods adding methods to tc.getFullName()"
+ def jobName = tc.getFullName()
+ def jobGroup = tc.getGroup()
+ def jobKey = tc.jobKey
+
+ //tc.clazz.metaClass.static.getGrailsApplication
+ tc.clazz.metaClass.getConfig = { ->
+ return application.mergedConfig
+ }
+
+ mc.'static'.schedule = { String cronExpression, Map params = null ->
+ //Trigger trigger = new CronTriggerImpl(generateTriggerName(), Constants.DEFAULT_TRIGGERS_GROUP, jobName, jobGroup, cronExpression)
+ def trigger = TriggerHelper.cronTrigger(jobKey,cronExpression,params)
+ quartzScheduler.scheduleJob(trigger)
+ }
+ mc.'static'.schedule = {Long interval, Integer repeatCount = SimpleTrigger.REPEAT_INDEFINITELY, Map params = null ->
+ //Trigger trigger = new SimpleTriggerImpl(generateTriggerName(), Constants.DEFAULT_TRIGGERS_GROUP, jobName, jobGroup, new Date(), null, repeatCount, interval)
+ def trigger = TriggerHelper.simpleTrigger(jobKey,new Date(),repeatCount,interval,params)
+ quartzScheduler.scheduleJob(trigger)
+ }
+ mc.'static'.schedule = {Date scheduleDate ->
+ //Trigger trigger = new SimpleTrigger(generateTriggerName(), Constants.DEFAULT_TRIGGERS_GROUP, jobName, jobGroup, scheduleDate, null, 0, 0)
+ def trigger = TriggerHelper.simpleTrigger(jobKey,scheduleDate,0,0,null)
+ quartzScheduler.scheduleJob(trigger)
+ }
+ mc.'static'.schedule = {Date scheduleDate, Map params ->
+ //Trigger trigger = new SimpleTrigger(generateTriggerName(), Constants.DEFAULT_TRIGGERS_GROUP, jobName, jobGroup, scheduleDate, null, 0, 0)
+ def trigger = TriggerHelper.simpleTrigger(jobKey,scheduleDate,0,0,params)
+ quartzScheduler.scheduleJob(trigger)
+ }
+ mc.'static'.schedule = {Trigger trigger ->
+ trigger.jobKey = jobKey
+ quartzScheduler.scheduleJob(trigger)
+ }
+ mc.'static'.triggerNow = { Map params = null ->
+ quartzScheduler.triggerJob(jobKey, params ? new JobDataMap(params) : null)
+ }
+ mc.'static'.removeJob = {
+ quartzScheduler.deleteJob(jobKey)
+ }
+
+ mc.'static'.reschedule = {Trigger trigger ->
+ trigger.jobKey = jobKey
+ quartzScheduler.rescheduleJob(trigger.key, trigger)
+ }
+
+ mc.'static'.unschedule = {String triggerName, String triggerGroup = Constants.DEFAULT_TRIGGERS_GROUP ->
+ quartzScheduler.unscheduleJob(new TriggerKey(triggerName,triggerGroup))
+ }
+
+ }
}
- def doWithApplicationContext = { ctx ->
- if(application.mergedConfig.grails.plugins.quartz2.autoStartup){
+
+ def doWithApplicationContext = {ctx ->
+
+ application.jobClasses.each {jobClass ->
+ //println "** doWithApplicationContext adding methods to jobClass.getFullName()"
+ scheduleJob.delegate = delegate
+ scheduleJob(jobClass, ctx)
+ }
+
+ if(application.mergedConfig.grails.plugin.quartz2.autoStartup){
def builders = application.mergedConfig.grails.plugin.quartz2.jobSetup.flatten()
if(builders?.keySet()){
builders.each{key,clos->
clos(ctx.quartzScheduler,ctx)
}
}
}
- // TODO Implement post initialization spring config (optional)
}
- def onChange = { event ->
- // TODO Implement code that is executed when any artefact that this plugin is
- // watching is modified and reloaded. The event contains: event.source,
- // event.application, event.manager, event.ctx, and event.plugin.
+ def onChange = {event ->
+ if (application.isArtefactOfType(JobArtefactHandler.TYPE, event.source)) {
+ log.debug("Job ${event.source} changed. Reloading...")
+ def context = event.ctx
+ def scheduler = context?.getBean("quartzScheduler")
+ // get quartz scheduler
+ if (context && scheduler) {
+ // if job already exists, delete it from scheduler
+ def jobClass = application.getJobClass(event.source?.name)
+ if (jobClass) {
+ scheduler.deleteJob(jobClass.jobKey)
+ log.debug("Job ${jobClass.fullName} deleted from the scheduler")
+ }
+
+ // add job artefact to application
+ jobClass = application.addArtefact(JobArtefactHandler.TYPE, event.source)
+
+ // configure and register job beans
+ def fullName = jobClass.fullName
+ def beans = beans {
+ configureJobBeans.delegate = delegate
+ configureJobBeans(jobClass)
+ }
+
+ context.registerBeanDefinition("${fullName}Class", beans.getBeanDefinition("${fullName}Class"))
+ context.registerBeanDefinition("${fullName}", beans.getBeanDefinition("${fullName}"))
+ context.registerBeanDefinition("${fullName}Detail", beans.getBeanDefinition("${fullName}Detail"))
+
+ // jobClass.triggers.each {name, trigger ->
+ // event.ctx.registerBeanDefinition("${name}Trigger", beans.getBeanDefinition("${name}Trigger"))
+ // }
+
+ scheduleJob(jobClass, event.ctx)
+ } else {
+ log.error("Application context or Quartz Scheduler not found. Can't reload Quartz plugin.")
+ }
+ }
}
- def onConfigChange = { event ->
- // TODO Implement code that is executed when the project configuration changes.
- // The event is the same as for 'onChange'.
+ def scheduleJob = {GrailsJobClass jobClass, ApplicationContext ctx ->
+ def scheduler = ctx.getBean("quartzScheduler")
+ if (scheduler) {
+ def fullName = jobClass.fullName
+ // add job to scheduler, and associate triggers with it
+ def jobDetail = ctx.getBean("${fullName}Detail")
+ scheduler.addJob(jobDetail, true)
+ jobClass.triggers.each {key, trigger ->
+ //println("Scheduling $fullName with trigger $key: ${trigger} with name:${trigger.triggerAttributes.name}")
+ def tkey = new TriggerKey(trigger.triggerAttributes.name,trigger.triggerAttributes.group)
+ def trigInstance = TriggersBuilder.createTrigger(trigger,jobDetail.key)
+ if (scheduler.getTrigger(tkey)) {
+ scheduler.rescheduleJob(tkey, trigInstance)
+ } else {
+ scheduler.scheduleJob(trigInstance)
+ }
+ }
+ //println("Job ${jobClass.fullName} scheduled")
+ } else {
+ log.error("Failed to register job triggers: scheduler not found")
+ }
}
- //private ConfigObject loadQuartzConfig(config) {
+ def configureJobBeans = {GrailsJobClass jobClass ->
+ def fullName = jobClass.fullName
+
+ "${fullName}Class"(MethodInvokingFactoryBean) {
+ targetObject = ref("grailsApplication", true)
+ targetMethod = "getArtefact"
+ arguments = [JobArtefactHandler.TYPE, jobClass.fullName]
+ }
+
+ "${fullName}"(ref("${fullName}Class")) {bean ->
+ bean.factoryMethod = "newInstance"
+ bean.autowire = "byName"
+ bean.scope = "prototype"
+ }
+
+ "${fullName}Detail"(GrailsArtefactJobDetailFactoryBean) {
+ grailsJobClass = jobClass
+ }
+
+ // registering triggers
+ // jobClass.triggers.each {name, trigger ->
+ // "${name}Trigger"(trigger.clazz) {
+ // jobDetail = ref("${fullName}Detail")
+ // trigger.properties.findAll {it.key != 'clazz'}.each {
+ // delegate["${it.key}"] = it.value
+ // }
+ // }
+ // }
+ }
+
Properties loadQuartzConfig(config) {
def properties = new Properties()
if (config.org.containsKey('quartz')) {
View
@@ -1,24 +1,31 @@
# A simple plugin for Quartz 2+ #
-Uses the new [Quartz][] 2.1 framework from quartz-scheduler.org. The goal is to keep it as simple as possible while making it friendly for Groovy/Grails.
+Uses the new [Quartz][] 2.1 framework from quartz-scheduler.org. The goal is to keep this as simple as possible while making it friendly for Groovy/Grails. This is (mostly) backward compatible with the original [Quartz plugin][] too so the concept of Job artifacts should work as well.
## What this plugin adds to be friendly with Grails
+* Its mostly backward compatible with the original [Quartz plugin][] so its allows scheduling jobs using job arifacts as well. See note below for areas where it is not compatible.
* Uses a factory to creates a single bean called quartzScheduler which is a standard Quartz [Scheduler][] and starts it. Its does not start it by default in test. You can inject and use the quartzScheduler bean like any normal Grails/Spring bean.
* All quartz settings can be done in Config.groovy, thus eliminating the need for a quartz.properties
-* Sets up a PersistenceContextJobListener, makes it a bean and adds it to the scheduler. This wraps all the jobs to make sure they have a hibernate session bound to the thread or if using another (nosql) engine then this should work for other non-hibernate gorm engines too as it uses the "persistenceInterceptor" bean to init(). If you don't need gorm persistence in your job then you can avoid the overhead and turn it of by assigning a "gorm:false" property in the the JobDataMap when setting up a [JobDetail][] or Trigger. Note:
+* Sets up a PersistenceContextJobListener, makes it a bean and adds it to the scheduler. This wraps all the jobs to make sure they have a hibernate session bound to the thread or if using another (nosql) engine then this should work for other non-hibernate gorm engines too as it uses the "persistenceInterceptor" bean to init(). If you don't need gorm persistence in your job then you can avoid the overhead and turn it of by assigning a "gormSession:false" property in the the JobDataMap when setting up a [JobDetail][] or Trigger. Note:
* adds a general InvokeMethodJob class that can be used to setup a [JobDetail][] to calls a service bean method or any static or local method on a passed in object
* support for assigning a builder closures in Config.groovy (or an externalized config) that will get called on application startup to setup your scheduler
* Adds a SimpleJobDetail - an implementation of the JobDetail that makes it easier to setDisallowConcurrentExecution with the need to put the annotaion on the Job class. Also makes it much easier to add JobDataMap properties by simply passing a map into the constructor
* ClosureJob - implements the Quartz [Job][] interface and is a utility class to allow you pass in configuration and a closure to be called when the Job executes.
-## Why we chose not to use or modify the quartz-plugin
+## Additions and changes from [Quartz plugin][]
-* the changes in [Quartz][] 2 made for many incompatibilities with older 1.8. I think it will be difficult to have 1 plugin support both versions but it may be possible with some work. Spring 3.1 seemed to pull it off but with a considerable amount of ugly gyrations
-* This plugin does not rely on the Spring support classes for quartz which the existing quartz-plugin makes heavy use of. Spring added support for [Quartz][] 2 in their upcoming 3.1 which will come with Grails 2. However we need and wanted Quartz 2 support now for our 1.3.x Grails apps
-* We wanted something dirt simple and light weight but got the job done to integrate with Grails
+outside of the ability to setup quartz jobs through config as outlined above and below, the following changes were made
-[Quartz][] 2 has a fairly simple way to build schedules so we just stick with the out of the box stuff. The [documentation and quick start][] are a fairly easy read.
+* Compatible with Grails 2x
+* the configuration and quartz.properties can now all reside in the standard Config.groovy ( or externalized config.groovy) instead needing to be separate files
+* uses the persistenceInterceptor spring bean to setup sessions instead of hibernate and sessionFactory allowing it to work better with other noSql plugins and the datasources plugins
+* Removed any dependency on the Spring wrapper classes around quartz
+* Removed deprecated volatility settings
+* Triggers are no longer setup as spring beans
+* Job artifact in the grails-app/jobs dir :
+* The config no longer needs to be static and can be completely extracted into a Config
+* a config property is injected into the Job artifact to make it easier to access it for setup. This injected property is actually a mergedConfig form the [plugin-config][]. This allows a plugin to setup a job with defaults and then allows the user of the plugin to override the settings much easier with more flexibility in the app (for example, change a trigger from a SimpleTrigger to a CronTrigger with more fine tuned control)
## Docs and Examples ##
@@ -35,7 +42,7 @@ You can externalize the config (see the grails docs on externalizing the config)
import static org.quartz.TriggerBuilder.*;
import grails.plugin.quartz2.InvokeMethodJob
- grails.plugins.quartz.autoStartup = true
+ grails.plugin.quartz2.autoStartup = true
org{
quartz{
@@ -101,8 +108,8 @@ name and jobClass are the only required fields
def sd = new SimpleJobDetail("test",TestJob.class, [prop:'xyz'] )
...
- //gorm:false will turn off the session init for gorm
- def map = [name:"test",jobClass:TestJob.class, concurrent:false, jobData:[fly:'free',gorm:false] ]
+ //gormSession:false will turn off the session init for gorm
+ def map = [name:"test",jobClass:TestJob.class, concurrent:false, jobData:[fly:'free',gormSession:false] ]
def sd = new SimpleJobDetail(map)
assert sd.isConcurrentExectionDisallowed() == false
assert sd.jobDataMap.fly=='free'
@@ -123,7 +130,7 @@ To pass in values for the JobDataMap then just pass in a map of values to the jo
println "************* it ran ***********"
//do something
}
- jobDetail.jobData = [gorm:false]
+ jobDetail.jobData = [gormSession:false]
def trigger = TriggerBuilder.newTrigger().withIdentity("closureJobTrigger")
.withSchedule(
@@ -190,10 +197,44 @@ Example:
}
+### Job Artifact Example ###
+
+
+
+Example:
+
+ class ConfigTriggerJob{
+
+ def concurrent = false
+
+ def getTriggers(){
+ return config.grails.plugin.xyz.someTriggerConfig
+ }
+
+ def execute() {
+ //do something
+ }
+ }
+
+.. and setup the trigger builder just like it says in the docs here [Quartz plugin][]. example config:
+
+ grails.plugin.xyz.someTriggerConfig = {
+ //repeat every second
+ simple repeatInterval: 1000l, repeatCount:1
+ }
+
+
+## Why we chose not to use or modify the quartz-plugin
+
+* the changes in [Quartz][] 2 made for many incompatibilities with older 1.8. I think it will be difficult to have 1 plugin support both versions but it may be possible with some work. Spring 3.1 seemed to pull it off but with a considerable amount of ugly gyrations
+* This plugin does not rely on the Spring support classes for quartz which the existing quartz-plugin does. Spring added support for [Quartz][] 2 in their upcoming 3.1 which will come with Grails 2. However we need and wanted Quartz 2 support now for our 1.3.x Grails apps
+* When doing jobs on the fly at customer sites, we wanted something dirt simple, light weight and used the quartz api but got the job done to integrate with Grails
[documentation and quick start]: http://www.quartz-scheduler.org/documentation/quartz-2.1.x/quick-start
[Quartz]: http://www.quartz-scheduler.org
[Job]: http://www.quartz-scheduler.org/api/2.1.0/org/quartz/Job.html
[JobDetail]: http://www.quartz-scheduler.org/api/2.1.0/org/quartz/JobDetail.html
[JobExecutionContext]: http://www.quartz-scheduler.org/api/2.1.0/org/quartz/JobExecutionContext.html
[Scheduler]: http://www.quartz-scheduler.org/api/2.1.0/org/quartz/impl/StdScheduler.html
+[Quartz plugin]: http://www.grails.org/plugin/quartz
+[plugin-config]: http://grails.org/plugin/plugin-config
Oops, something went wrong. Retry.

0 comments on commit cf4d0dc

Please sign in to comment.