Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

initial commit

  • Loading branch information...
commit 102ca7c48fc6b4d619a37cf02c3aa510988bebe8 0 parents
Joshua Burnett authored
Showing with 18,213 additions and 0 deletions.
  1. +11 −0 .gitignore
  2. +105 −0 Quartz2GrailsPlugin.groovy
  3. +199 −0 README.md
  4. +6 −0 application.properties
  5. +26 −0 grails-app/conf/BuildConfig.groovy
  6. +25 −0 grails-app/conf/Config.groovy
  7. +32 −0 grails-app/conf/DataSource.groovy
  8. +25 −0 grails-app/conf/Quartz2DefaultConfig.groovy
  9. +131 −0 quartz2.tmproj
  10. +10 −0 scripts/_Install.groovy
  11. +5 −0 scripts/_Uninstall.groovy
  12. +10 −0 scripts/_Upgrade.groovy
  13. +77 −0 src/groovy/grails/plugin/quartz2/ClosureJob.groovy
  14. +97 −0 src/groovy/grails/plugin/quartz2/InvokeMethodJob.groovy
  15. +38 −0 src/groovy/grails/plugin/quartz2/JobErrorLoggerListener.groovy
  16. +58 −0 src/groovy/grails/plugin/quartz2/PersistenceContextJobListener.groovy
  17. +180 −0 src/groovy/grails/plugin/quartz2/QuartzFactoryBean.groovy
  18. +44 −0 src/groovy/grails/plugin/quartz2/QuartzHelper.groovy
  19. +223 −0 src/groovy/grails/plugin/quartz2/SimpleJobDetail.groovy
  20. +132 −0 test/projects/qkiss/app-qkiss-quartz.groovy
  21. +8 −0 test/projects/qkiss/application.properties
  22. +38 −0 test/projects/qkiss/grails-app/conf/BootStrap.groovy
  23. +32 −0 test/projects/qkiss/grails-app/conf/BuildConfig.groovy
  24. +60 −0 test/projects/qkiss/grails-app/conf/Config.groovy
  25. +32 −0 test/projects/qkiss/grails-app/conf/DataSource.groovy
  26. +13 −0 test/projects/qkiss/grails-app/conf/UrlMappings.groovy
  27. +3 −0  test/projects/qkiss/grails-app/conf/spring/resources.groovy
  28. +9 −0 test/projects/qkiss/grails-app/domain/qkiss/Org.groovy
  29. +55 −0 test/projects/qkiss/grails-app/i18n/messages.properties
  30. +56 −0 test/projects/qkiss/grails-app/i18n/messages_da.properties
  31. +55 −0 test/projects/qkiss/grails-app/i18n/messages_de.properties
  32. +30 −0 test/projects/qkiss/grails-app/i18n/messages_es.properties
  33. +19 −0 test/projects/qkiss/grails-app/i18n/messages_fr.properties
  34. +19 −0 test/projects/qkiss/grails-app/i18n/messages_it.properties
  35. +19 −0 test/projects/qkiss/grails-app/i18n/messages_ja.properties
  36. +55 −0 test/projects/qkiss/grails-app/i18n/messages_nl.properties
  37. +34 −0 test/projects/qkiss/grails-app/i18n/messages_pt_BR.properties
  38. +34 −0 test/projects/qkiss/grails-app/i18n/messages_pt_PT.properties
  39. +31 −0 test/projects/qkiss/grails-app/i18n/messages_ru.properties
  40. +35 −0 test/projects/qkiss/grails-app/i18n/messages_th.properties
  41. +18 −0 test/projects/qkiss/grails-app/i18n/messages_zh_CN.properties
  42. +27 −0 test/projects/qkiss/grails-app/services/qkiss/HunterService.groovy
  43. +54 −0 test/projects/qkiss/grails-app/views/error.gsp
  44. +100 −0 test/projects/qkiss/grails-app/views/index.gsp
  45. +17 −0 test/projects/qkiss/grails-app/views/layouts/main.gsp
  46. +36 −0 test/projects/qkiss/src/groovy/qkiss/HelloFromExternalConfigJob.groovy
  47. +35 −0 test/projects/qkiss/src/groovy/qkiss/HelloJob.groovy
  48. +33 −0 test/projects/qkiss/test/integration/qkiss/BuilderConfigTests.groovy
  49. +73 −0 test/projects/qkiss/test/integration/qkiss/ClosureJobTests.groovy
  50. +55 −0 test/projects/qkiss/test/integration/qkiss/JobLookupTests.groovy
  51. +17 −0 test/projects/qkiss/test/unit/qkiss/JobRunnerServiceTests.groovy
  52. +17 −0 test/projects/qkiss/test/unit/qkiss/OrgTests.groovy
  53. +42 −0 test/projects/qkiss/web-app/WEB-INF/applicationContext.xml
  54. +14 −0 test/projects/qkiss/web-app/WEB-INF/sitemesh.xml
  55. +563 −0 test/projects/qkiss/web-app/WEB-INF/tld/c.tld
  56. +671 −0 test/projects/qkiss/web-app/WEB-INF/tld/fmt.tld
  57. +550 −0 test/projects/qkiss/web-app/WEB-INF/tld/grails.tld
  58. +311 −0 test/projects/qkiss/web-app/WEB-INF/tld/spring.tld
  59. +273 −0 test/projects/qkiss/web-app/css/main.css
  60. BIN  test/projects/qkiss/web-app/images/favicon.ico
  61. BIN  test/projects/qkiss/web-app/images/grails_logo.jpg
  62. BIN  test/projects/qkiss/web-app/images/grails_logo.png
  63. BIN  test/projects/qkiss/web-app/images/leftnav_btm.png
  64. BIN  test/projects/qkiss/web-app/images/leftnav_midstretch.png
  65. BIN  test/projects/qkiss/web-app/images/leftnav_top.png
  66. BIN  test/projects/qkiss/web-app/images/skin/database_add.png
  67. BIN  test/projects/qkiss/web-app/images/skin/database_delete.png
  68. BIN  test/projects/qkiss/web-app/images/skin/database_edit.png
  69. BIN  test/projects/qkiss/web-app/images/skin/database_save.png
  70. BIN  test/projects/qkiss/web-app/images/skin/database_table.png
  71. BIN  test/projects/qkiss/web-app/images/skin/exclamation.png
  72. BIN  test/projects/qkiss/web-app/images/skin/house.png
  73. BIN  test/projects/qkiss/web-app/images/skin/information.png
  74. BIN  test/projects/qkiss/web-app/images/skin/shadow.jpg
  75. BIN  test/projects/qkiss/web-app/images/skin/sorted_asc.gif
  76. BIN  test/projects/qkiss/web-app/images/skin/sorted_desc.gif
  77. BIN  test/projects/qkiss/web-app/images/spinner.gif
  78. BIN  test/projects/qkiss/web-app/images/springsource.png
  79. +13 −0 test/projects/qkiss/web-app/js/application.js
  80. +7 −0 test/projects/qkiss/web-app/js/prototype/animation.js
  81. +136 −0 test/projects/qkiss/web-app/js/prototype/builder.js
  82. +965 −0 test/projects/qkiss/web-app/js/prototype/controls.js
  83. +974 −0 test/projects/qkiss/web-app/js/prototype/dragdrop.js
  84. +1,123 −0 test/projects/qkiss/web-app/js/prototype/effects.js
  85. +4,874 −0 test/projects/qkiss/web-app/js/prototype/prototype.js
  86. +2,691 −0 test/projects/qkiss/web-app/js/prototype/rico.js
  87. +68 −0 test/projects/qkiss/web-app/js/prototype/scriptaculous.js
  88. +275 −0 test/projects/qkiss/web-app/js/prototype/slider.js
  89. +59 −0 test/projects/qkiss/web-app/js/prototype/sound.js
  90. +568 −0 test/projects/qkiss/web-app/js/prototype/unittest.js
  91. +84 −0 test/unit/grails/plugin/quartz2/ClosureJobTests.groovy
  92. +65 −0 test/unit/grails/plugin/quartz2/InvokeMethodJobTests.groovy
  93. +44 −0 test/unit/grails/plugin/quartz2/SimpleJobDetailTests.groovy
  94. +42 −0 web-app/WEB-INF/applicationContext.xml
  95. +14 −0 web-app/WEB-INF/sitemesh.xml
  96. +563 −0 web-app/WEB-INF/tld/c.tld
  97. +671 −0 web-app/WEB-INF/tld/fmt.tld
11 .gitignore
@@ -0,0 +1,11 @@
+target/
+stacktrace.log
+
+# Eclipse Project files
+.classpath
+.project
+.settings/
+
+# OS X generated files
+.DS_Store
+
105 Quartz2GrailsPlugin.groovy
@@ -0,0 +1,105 @@
+import grails.plugin.quartz2.*
+
+class Quartz2GrailsPlugin {
+ // the plugin version
+ def version = "0.2.1"
+ // the version or versions of Grails the plugin is designed for
+ def grailsVersion = "1.3.1 > *"
+ // the other plugins this plugin depends on
+ def dependsOn = [:]
+ // resources that are excluded from plugin packaging
+ def pluginExcludes = [
+ "grails-app/views/error.gsp"
+ ]
+
+ // TODO Fill in these fields
+ def author = "Joshua Burentt"
+ def authorEmail = "Joshua@greenbill.com"
+ def title = "Quartz 2.1 Scheduler"
+ def description = '''\\
+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.
+'''
+
+ // 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 doWithSpring = {
+ def mcfg = application.mergedConfig
+ def quartzProps = loadQuartzConfig(mcfg)
+
+ persistenceContextJobListener(PersistenceContextJobListener){
+ persistenceInterceptor = ref("persistenceInterceptor")
+ }
+
+ jobErrorLoggerListener(JobErrorLoggerListener)
+
+ quartzScheduler(QuartzFactoryBean) {
+ grailsApplication = ref('grailsApplication')
+ quartzProperties = quartzProps
+ // delay scheduler startup to after-bootstrap stage
+ autoStartup = mcfg.grails.plugins.quartz2.autoStartup
+ globalJobListeners = [ref('jobErrorLoggerListener'),ref('persistenceContextJobListener')]
+ }
+
+ /* for future reloading
+ scheduledExecutorForReloading(org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean){
+ continueScheduledExecutionAfterException=true
+ waitForTasksToCompleteOnShutdown=true
+ scheduledExecutorTasks = [ref('scheduledExecutorTaskForReloading')]
+ }
+ scheduledExecutorTaskForReloading(org.springframework.scheduling.concurrent.ScheduledExecutorTask){
+ runnable = ref('configReloadingTask')
+ delay=5000 //5 seconds
+ period=5000 //5 seconds
+ }
+
+ configReloadingTask(ConfigReloadingTask)
+ */
+
+ }
+
+ def doWithDynamicMethods = { ctx ->
+ // TODO Implement registering dynamic methods to classes (optional)
+ }
+
+ def doWithApplicationContext = { ctx ->
+ if(application.mergedConfig.grails.plugins.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 onConfigChange = { event ->
+ // TODO Implement code that is executed when the project configuration changes.
+ // The event is the same as for 'onChange'.
+ }
+
+ //private ConfigObject loadQuartzConfig(config) {
+ Properties loadQuartzConfig(config) {
+ def properties = new Properties()
+ if (config.org.containsKey('quartz')) {
+ properties << config.org.quartz.toProperties('org.quartz')
+ }
+
+ //config.quartz._properties = properties
+
+ return properties
+ }
+
+}
199 README.md
@@ -0,0 +1,199 @@
+# 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.
+
+## What this plugin adds to be friendly with Grails
+
+* 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:
+* 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
+
+* 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
+
+[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.
+
+## Docs and Examples ##
+
+clone this and look at the example test project under tests/projects/qkiss for examples on how its being used. Look at Config.groovy, the externalized app-qkiss-config.groovy on the root, and the integration tests. To get a schedule going in your app follow the examples in the [documentation and quick start][] and either set a grails.plugin.quartz2.jobSetup closure in config as in the example below or just inject the quartzScheduler([Scheduler][]) into BuildConfig and call quartzScheduler.scheduleJob once you have a JobDetail and Trigger setup.
+
+### Config.groovy closure
+
+You can externalize the config (see the grails docs on externalizing the config) and this allows us to configure and setup new jobs without recompiling the app. For example you could add something like grails.config.locations = [ "file:Quartz-config.groovy"] in your config.groovy and then follow the example below.
+
+#### Example Quartz-config.groovy
+
+ import static org.quartz.JobBuilder.*;
+ import static org.quartz.SimpleScheduleBuilder.*;
+ import static org.quartz.TriggerBuilder.*;
+ import grails.plugin.quartz2.InvokeMethodJob
+
+ grails.plugins.quartz.autoStartup = true
+
+ org{
+ quartz{
+ //anything here will get merged into the quartz.properties so you don't need another file
+ scheduler.instanceName = 'MyAppScheduler'
+ threadPool.class = 'org.quartz.simpl.SimpleThreadPool'
+ threadPool.threadCount = 20
+ threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
+ jobStore.class = 'org.quartz.simpl.RAMJobStore'
+ }
+ }
+
+ /you can drive the setup. just give them a unique key like "buyTheTicket" below.
+ //the quartzScheduler bean and application context are passed to your closure
+ grails.plugin.quartz2.jobSetup.buyTheTicket = { quartzScheduler, ctx ->
+ //how it should look
+ def jobDetail = ClosureJob.createJob { jobCtx , appCtx->
+ appCtx.hunterService.takeTheRide(true)
+ }
+
+ def trigger1 = new SimpleTriggerImpl(name:"trig1", startTime:new Date(),repeatInterval:1000,repeatCount:-1)
+
+ quartzScheduler.scheduleJob(jobDetail, trigger1)
+ }
+
+ grails.plugin.quartz2.jobSetup.buyTicket2 = { quartzScheduler, ctx ->
+
+ //example a service call using the InvokeMethodJob and quartz's new builder syntax
+ def props = new JobDataMap([targetObject:ctx.hunterService,targetMethod:'takeTheRide',arguments:[true]])
+ JobDetail jobDetail = newJob(InvokeMethodJob.class) //use the static helper newJob from org.quartz.JobBuilder
+ .withIdentity("take the ride")
+ .usingJobData(props)
+ .build()
+
+ Trigger trigger = newTrigger().withIdentity("hunter trigger")
+ .withSchedule(
+ simpleSchedule().withIntervalInSeconds(1).repeatForever()
+ )
+ .startNow().build()
+
+ quartzScheduler.scheduleJob(jobDetail, trigger)
+ }
+
+
+### SimpleJobDetail ###
+
+SimpleJobDetail is an implementation of the Quartz [JobDetail][] interface.
+Its primary purpose was to make it easier to set make a Job or JobDetail single threaded and not need to
+name and jobClass are the only required fields
+
+- **name** * - the jobKey name for this
+- **group** - the jobKey group name
+- **key** - set your own JobKey instead of constructing from a map
+- **description** - description
+- **jobClass** * - the Job class that will be instantiated and run via the exceute method
+- **jobDataMap** - a JobDataMap with any data that will be set to the JobExecutionContext
+- **jobData** - a convenience setter to allow a normal Map to be set into the JobDataMap
+- **concurrent** - setting this to false will make it so only 1 trigger at a time can run. isConcurrentExectionDisallowed will return true.
+- **durability** - if this be stored even if no trigers
+- **persistJobDataAfterExecution**
+
+#### examples
+
+ 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] ]
+ def sd = new SimpleJobDetail(map)
+ assert sd.isConcurrentExectionDisallowed() == false
+ assert sd.jobDataMap.fly=='free'
+
+### ClosureJob ###
+
+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.
+the easiest way is to use the static method **createJob** which will return a **SimpleJobDetail**.
+You can pass in a map of parameters to construct the SimpleJobDetail and then a closure that takes optional args for [JobExecutionContext][] and the spring application context .
+To pass in values for the JobDataMap then just pass in a map of values to the jobData property such as
+
+#### examples
+
+ //import the static as in previous examples
+ def dataMap
+
+ def jobDetail = ClosureJob.createJob(name:"test_quarts_builder",durability:true,concurrent:false){ jobCtx , appCtx->
+ println "************* it ran ***********"
+ //do something
+ }
+ jobDetail.jobData = [gorm:false]
+
+ def trigger = TriggerBuilder.newTrigger().withIdentity("closureJobTrigger")
+ .withSchedule(
+ simpleSchedule()
+ .withIntervalInMilliseconds(10)
+ .withRepeatCount(2)
+ ).startNow().build()
+
+ quartzScheduler.scheduleJob(jobDetail, trigger)
+
+### InvokeMethodJob ###
+
+InvokeMethodJob implements the Quartz [Job][] interface and facilitates calls to groovy's object.invokeMethod( targetMethod, args) or the metaClass.invokeStaticMethod.
+It is configured by setting up the JobDataMap with the following keys
+
+- **targetObject** - the object to call the method on.
+- **targetClass** - set this instead of targetObject if targetMethod is a static
+- **targetMethod** - the method to call on the object, or static method to call on the targetClass
+- **arguments** - list or array of arguments that you want passed to your method.
+
+#### InvokeMethodJob examples
+
+ import static org.quartz.JobBuilder.*
+ import static org.quartz.SimpleScheduleBuilder.*
+ import static org.quartz.TriggerBuilder.*
+ import static org.quartz.DateBuilder.*
+ import grails.plugin.quartz2.InvokeMethodJob
+ ...
+ def jobDataMap = new JobDataMap([targetObject:ctx.hunterService,targetMethod:'takeTheRide',arguments:[true]])
+ JobDetail job2 = newJob(InvokeMethodJob.class).withIdentity("take the ride")
+ .usingJobData(jobDataMap)
+ .build()
+
+ Trigger trigger2 = newTrigger().withIdentity("hunter trigger2")
+ .withSchedule(simpleSchedule().withIntervalInSeconds(4).repeatForever())
+ .startNow()
+ .build()
+
+ quartzScheduler.scheduleJob(job2, trigger2)
+
+### Setting up your own job class ###
+
+the plugin will set's the grailsAppication and appCtx (applicationContext) into "global" property into the schedules context.
+Quartz, by default , uses the PropertySettingJobFactory which will attempt to inject the properties by keyName that exists in the schedule;s context, jobs context or triggers context into the Job when it creates it. So that mean if you setup your own Job and need the appCtx or grailsApplication just declare a field and it will get set.
+
+Example:
+
+ public class HelloJob implements Job {
+
+ def grailsApplication
+
+ /**
+ * Quartz requires a public empty constructor so that the
+ * scheduler can instantiate the class whenever it needs.
+ */
+ public HelloJob() {}
+
+ void execute(JobExecutionContext jobCtx) {
+ def someConfigProp = grailsApplication.config.my.prop.here
+ //or get a service with grailsApplication.mainContext.someService
+ // Say Hello to the World and display the date/time
+ println ("Hello from - ${jobCtx.jobDetail.key.name} - $someConfigProp" )
+ }
+
+ }
+
+
+[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
6 application.properties
@@ -0,0 +1,6 @@
+#Grails Metadata file
+#Wed Oct 05 15:46:15 CDT 2011
+app.grails.version=1.3.7
+app.name=quartz2
+plugins.hibernate=1.3.7
+plugins.tomcat=1.3.7
26 grails-app/conf/BuildConfig.groovy
@@ -0,0 +1,26 @@
+grails.project.class.dir = "target/classes"
+grails.project.test.class.dir = "target/test-classes"
+grails.project.test.reports.dir = "target/test-reports"
+//grails.project.war.file = "target/${appName}-${appVersion}.war"
+grails.project.dependency.resolution = {
+ // inherit Grails' default dependencies
+ inherits("global") {
+ // uncomment to disable ehcache
+ // excludes 'ehcache'
+ }
+ log "warn" // log level of Ivy resolver, either 'error', 'warn', 'info', 'debug' or 'verbose'
+ repositories {
+ grailsPlugins()
+ grailsHome()
+ grailsCentral()
+ mavenCentral()
+ }
+ dependencies {
+ // specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes eg.
+ compile 'org.quartz-scheduler:quartz:2.1.0'
+ // runtime 'mysql:mysql-connector-java:5.1.13'
+ }
+ plugins {
+ compile ':plugin-config:0.1.5'
+ }
+}
25 grails-app/conf/Config.groovy
@@ -0,0 +1,25 @@
+// configuration for plugin testing - will not be included in the plugin zip
+
+log4j = {
+ // Example of changing the log pattern for the default console
+ // appender:
+ //
+ root { info() }
+ appenders {
+ console name:'stdout', layout:pattern(conversionPattern: '%c{2} %m%n')
+ }
+
+ error 'org.codehaus.groovy.grails.web.servlet', // controllers
+ 'org.codehaus.groovy.grails.web.pages', // GSP
+ 'org.codehaus.groovy.grails.web.sitemesh', // layouts
+ 'org.codehaus.groovy.grails.web.mapping.filter', // URL mapping
+ 'org.codehaus.groovy.grails.web.mapping', // URL mapping
+ 'org.codehaus.groovy.grails.commons', // core / classloading
+ 'org.codehaus.groovy.grails.plugins', // plugins
+ 'org.codehaus.groovy.grails.orm.hibernate', // hibernate integration
+ 'org.springframework',
+ 'org.hibernate',
+ 'net.sf.ehcache.hibernate'
+
+ warn 'org.mortbay.log'
+}
32 grails-app/conf/DataSource.groovy
@@ -0,0 +1,32 @@
+dataSource {
+ pooled = true
+ driverClassName = "org.hsqldb.jdbcDriver"
+ username = "sa"
+ password = ""
+}
+hibernate {
+ cache.use_second_level_cache = true
+ cache.use_query_cache = true
+ cache.provider_class = 'net.sf.ehcache.hibernate.EhCacheProvider'
+}
+// environment specific settings
+environments {
+ development {
+ dataSource {
+ dbCreate = "create-drop" // one of 'create', 'create-drop','update'
+ url = "jdbc:hsqldb:mem:devDB"
+ }
+ }
+ test {
+ dataSource {
+ dbCreate = "update"
+ url = "jdbc:hsqldb:mem:testDb"
+ }
+ }
+ production {
+ dataSource {
+ dbCreate = "update"
+ url = "jdbc:hsqldb:file:prodDb;shutdown=true"
+ }
+ }
+}
25 grails-app/conf/Quartz2DefaultConfig.groovy
@@ -0,0 +1,25 @@
+org{
+ quartz{
+ scheduler.skipUpdateCheck = true
+ scheduler.instanceName = 'DefaultPluginScheduler'
+ threadPool.class = 'org.quartz.simpl.SimpleThreadPool'
+ threadPool.threadCount = 10
+ threadPool.threadPriority = 5
+ threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
+ jobStore.class = 'org.quartz.simpl.RAMJobStore'
+ }
+}
+
+grails{
+ plugins{
+ quartz2 {
+ autoStartup = true
+ }
+ }
+}
+
+environments {
+ test {
+ grails.plugins.quartz2.autoStartup = false
+ }
+}
131 quartz2.tmproj
@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>currentDocument</key>
+ <string>QuartzKissGrailsPlugin.groovy</string>
+ <key>documents</key>
+ <array>
+ <dict>
+ <key>expanded</key>
+ <true/>
+ <key>name</key>
+ <string>grails-app</string>
+ <key>regexFolderFilter</key>
+ <string>!.*/(\.[^/]*|CVS|_darcs|_MTN|\{arch\}|blib|.*~\.nib|.*\.(framework|app|pbproj|pbxproj|xcode(proj)?|bundle))$</string>
+ <key>sourceDirectory</key>
+ <string>grails-app</string>
+ </dict>
+ <dict>
+ <key>name</key>
+ <string>scripts</string>
+ <key>regexFolderFilter</key>
+ <string>!.*/(\.[^/]*|CVS|_darcs|_MTN|\{arch\}|blib|.*~\.nib|.*\.(framework|app|pbproj|pbxproj|xcode(proj)?|bundle))$</string>
+ <key>sourceDirectory</key>
+ <string>scripts</string>
+ </dict>
+ <dict>
+ <key>name</key>
+ <string>src</string>
+ <key>regexFolderFilter</key>
+ <string>!.*/(\.[^/]*|CVS|_darcs|_MTN|\{arch\}|blib|.*~\.nib|.*\.(framework|app|pbproj|pbxproj|xcode(proj)?|bundle))$</string>
+ <key>sourceDirectory</key>
+ <string>src</string>
+ </dict>
+ <dict>
+ <key>expanded</key>
+ <true/>
+ <key>name</key>
+ <string>test</string>
+ <key>regexFolderFilter</key>
+ <string>!.*/(\.[^/]*|CVS|_darcs|_MTN|\{arch\}|blib|.*~\.nib|.*\.(framework|app|pbproj|pbxproj|xcode(proj)?|bundle))$</string>
+ <key>sourceDirectory</key>
+ <string>test</string>
+ </dict>
+ <dict>
+ <key>name</key>
+ <string>web-app</string>
+ <key>regexFolderFilter</key>
+ <string>!.*/(\.[^/]*|CVS|_darcs|_MTN|\{arch\}|blib|.*~\.nib|.*\.(framework|app|pbproj|pbxproj|xcode(proj)?|bundle))$</string>
+ <key>sourceDirectory</key>
+ <string>web-app</string>
+ </dict>
+ <dict>
+ <key>filename</key>
+ <string>QuartzKissGrailsPlugin.groovy</string>
+ <key>lastUsed</key>
+ <date>2011-12-13T17:23:56Z</date>
+ <key>selected</key>
+ <true/>
+ </dict>
+ <dict>
+ <key>filename</key>
+ <string>README.md</string>
+ <key>lastUsed</key>
+ <date>2011-12-11T20:20:38Z</date>
+ </dict>
+ <dict>
+ <key>filename</key>
+ <string>application.properties</string>
+ <key>lastUsed</key>
+ <date>2011-10-13T20:29:48Z</date>
+ </dict>
+ </array>
+ <key>fileHierarchyDrawerWidth</key>
+ <integer>400</integer>
+ <key>metaData</key>
+ <dict>
+ <key>QuartzKissGrailsPlugin.groovy</key>
+ <dict>
+ <key>caret</key>
+ <dict>
+ <key>column</key>
+ <integer>9</integer>
+ <key>line</key>
+ <integer>45</integer>
+ </dict>
+ <key>firstVisibleColumn</key>
+ <integer>0</integer>
+ <key>firstVisibleLine</key>
+ <integer>29</integer>
+ </dict>
+ <key>README.md</key>
+ <dict>
+ <key>caret</key>
+ <dict>
+ <key>column</key>
+ <integer>184</integer>
+ <key>line</key>
+ <integer>8</integer>
+ </dict>
+ <key>firstVisibleColumn</key>
+ <integer>0</integer>
+ <key>firstVisibleLine</key>
+ <integer>105</integer>
+ </dict>
+ <key>src/groovy/grails/plugin/quartzkiss/QuartzHelper.groovy</key>
+ <dict>
+ <key>caret</key>
+ <dict>
+ <key>column</key>
+ <integer>13</integer>
+ <key>line</key>
+ <integer>41</integer>
+ </dict>
+ <key>firstVisibleColumn</key>
+ <integer>0</integer>
+ <key>firstVisibleLine</key>
+ <integer>6</integer>
+ </dict>
+ </dict>
+ <key>openDocuments</key>
+ <array>
+ <string>README.md</string>
+ <string>QuartzKissGrailsPlugin.groovy</string>
+ </array>
+ <key>showFileHierarchyDrawer</key>
+ <true/>
+ <key>windowFrame</key>
+ <string>{{65, 0}, {1445, 1178}}</string>
+</dict>
+</plist>
10 scripts/_Install.groovy
@@ -0,0 +1,10 @@
+//
+// This script is executed by Grails after plugin was installed to project.
+// This script is a Gant script so you can use all special variables provided
+// by Gant (such as 'baseDir' which points on project base dir). You can
+// use 'ant' to access a global instance of AntBuilder
+//
+// For example you can create directory under project tree:
+//
+// ant.mkdir(dir:"${basedir}/grails-app/jobs")
+//
5 scripts/_Uninstall.groovy
@@ -0,0 +1,5 @@
+//
+// This script is executed by Grails when the plugin is uninstalled from project.
+// Use this script if you intend to do any additional clean-up on uninstall, but
+// beware of messing up SVN directories!
+//
10 scripts/_Upgrade.groovy
@@ -0,0 +1,10 @@
+//
+// This script is executed by Grails during application upgrade ('grails upgrade'
+// command). This script is a Gant script so you can use all special variables
+// provided by Gant (such as 'baseDir' which points on project base dir). You can
+// use 'ant' to access a global instance of AntBuilder
+//
+// For example you can create directory under project tree:
+//
+// ant.mkdir(dir:"${basedir}/grails-app/jobs")
+//
77 src/groovy/grails/plugin/quartz2/ClosureJob.groovy
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+
+package grails.plugin.quartz2
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+//import org.quartz.DisallowConcurrentExecution
+
+/**
+ * Quartz Job implementation that invokes a Groovy closure
+ */
+public static class ClosureJob implements Job {
+ private static Logger log = LoggerFactory.getLogger(ClosureJob.class);
+
+ Closure groovyClosure
+ def applicationContext
+
+ public ClosureJob() {}
+
+ void execute(JobExecutionContext jobCtx) {
+ log.debug("ClosureJob")
+ try{
+ int argNum = groovyClosure.getMaximumNumberOfParameters()
+ if( argNum == 2){
+ groovyClosure(jobCtx,applicationContext)
+ }else if(argNum ==1){
+ groovyClosure(jobCtx)
+ }else{
+ groovyClosure()
+ }
+
+ }catch(JobExecutionException e){
+ throw e
+ }
+ catch(e){
+ throw new JobExecutionException(e)
+ }
+
+ }
+
+ static SimpleJobDetail createJob(Closure c){
+ def det = new SimpleJobDetail()
+ det.jobClass = ClosureJob.class
+ det.jobDataMap.putAll([groovyClosure:c])
+ return det
+ }
+
+ static SimpleJobDetail createJob(String name, Closure c){
+ def jobDet = new SimpleJobDetail(name,ClosureJob.class, [groovyClosure:c])
+ return jobDet
+ }
+
+ static SimpleJobDetail createJob(Map params, Closure c){
+ def det = new SimpleJobDetail(params)
+ det.jobClass = ClosureJob.class
+ det.jobDataMap.putAll([groovyClosure:c])
+ return det
+ }
+
+}
97 src/groovy/grails/plugin/quartz2/InvokeMethodJob.groovy
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+
+package grails.plugin.quartz2
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+//import org.quartz.DisallowConcurrentExecution
+
+/**
+ * Quartz Job implementation that invokes a specified method of an object, or static if class is specified.
+ * this has the DisallowConcurrentExecution set to true
+ */
+public static class InvokeMethodJob implements Job {
+ private static Logger log = LoggerFactory.getLogger(InvokeMethodJob.class);
+
+ Class targetClass //the class if this is a static
+ Object targetObject
+ //String targetBeanName
+ String targetMethod
+ //String staticMethod
+ def arguments
+ def grailsApplication
+
+ public InvokeMethodJob() {}
+
+ void execute(JobExecutionContext jobCtx) {
+ log.debug("BeanMethodJob called for $targetObject method:$targetMethod targetClass:${targetClass?.name} arguments:$arguments")
+ try{
+ if(targetClass){
+ invokeStatic( jobCtx)
+ }
+ else if(targetObject){
+ invokeNormal( jobCtx)
+ }
+ else{
+ throw new JobExecutionException("Either targetClass or targetObject need to be set for InvokeMethodJob.")
+ }
+ }catch(JobExecutionException e){
+ throw e
+ }
+ catch(e){
+ throw new JobExecutionException(e)
+ }
+
+ }
+
+ void invokeNormal(JobExecutionContext jobCtx){
+ try {
+ if(!targetMethod) throw new JobExecutionException("targetMethod must be specified")
+ jobCtx.result = targetObject.invokeMethod( targetMethod, makeArgs())
+ }
+ catch(JobExecutionException e){
+ throw e
+ }
+ catch(e){
+ throw new JobExecutionException(e)
+ }
+ }
+
+ void invokeStatic(JobExecutionContext jobCtx){
+ try{
+ jobCtx.result = targetClass.metaClass.invokeStaticMethod(targetClass, targetMethod, makeArgs())
+ }catch(JobExecutionException e){
+ throw e
+ }
+ catch(e){
+ throw new JobExecutionException(e)
+ }
+ }
+
+ Object[] makeArgs(){
+ if(!arguments) return new Object[0]
+ if(arguments.class.isArray()) return arguments
+ if(arguments instanceof Collection) return arguments.toArray()
+ //its a single argument then
+ def o = new Object[1]
+ o[0]=arguments
+ return o
+ }
+}
38 src/groovy/grails/plugin/quartz2/JobErrorLoggerListener.groovy
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+package grails.plugin.quartz2
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.quartz.listeners.JobListenerSupport;
+
+public class JobErrorLoggerListener extends JobListenerSupport {
+ private static final transient Log log = LogFactory.getLog(JobErrorLoggerListener.class);
+
+ public static final String NAME = "jobErrorLoggerListener"
+
+ public String getName() {
+ return NAME
+ }
+
+ public void jobWasExecuted(JobExecutionContext ctx, JobExecutionException ex) {
+ if (log.debugEnabled) log.debug("job:$ctx.jobDetail.key.name was executed")
+ if (ex) log.error("Exception thrown in job:$ctx.jobDetail.key.name", ex)
+ }
+}
58 src/groovy/grails/plugin/quartz2/PersistenceContextJobListener.groovy
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+package grails.plugin.quartz2
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.quartz.listeners.JobListenerSupport;
+
+import org.codehaus.groovy.grails.support.PersistenceContextInterceptor
+
+public class PersistenceContextJobListener extends JobListenerSupport {
+ private static final transient Log log = LogFactory.getLog(PersistenceContextJobListener.class);
+ PersistenceContextInterceptor persistenceInterceptor
+
+ public static final String NAME = "persistenceContextJobListener"
+
+ public String getName() {
+ return NAME
+ }
+
+ public void jobToBeExecuted(JobExecutionContext context) {
+ if( isInitPersistenceContext(context) ){
+ persistenceInterceptor.init()
+ }
+ }
+
+ public void jobWasExecuted(JobExecutionContext context, JobExecutionException exception) {
+ if( isInitPersistenceContext(context) ){
+ persistenceInterceptor.flush()
+ persistenceInterceptor.destroy()
+ }
+ }
+
+ boolean isInitPersistenceContext(context){
+ if(context.mergedJobDataMap.containsKey('gorm') && context.mergedJobDataMap.get('gorm') == false){
+ return false
+ }else{
+ return true
+ }
+ }
+}
180 src/groovy/grails/plugin/quartz2/QuartzFactoryBean.groovy
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2002-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.
+ */
+
+package grails.plugin.quartz2
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.quartz.impl.StdSchedulerFactory
+
+import org.quartz.Calendar;
+import org.quartz.JobDetail;
+import org.quartz.JobListener;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.SchedulerListener;
+import org.quartz.Trigger;
+import org.quartz.TriggerListener;
+
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.BeanNameAware;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.SmartLifecycle;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.io.support.PropertiesLoaderUtils;
+import org.springframework.scheduling.SchedulingException;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * A very simple factory bean for the Quartz Scheduler
+ * took a lot of ideas and used Spring's SchedulerFactoryBean as a starting point
+ *
+ * @author Joshua Burnett
+ * @author Juergen Hoeller wrote original SchedulerFactoryBean
+ */
+public class QuartzFactoryBean implements FactoryBean<Scheduler>, ApplicationContextAware,InitializingBean, DisposableBean, SmartLifecycle {
+ private static final transient Log log = LogFactory.getLog(JobErrorLoggerListener.class);
+
+ def grailsApplication
+ Scheduler scheduler
+ boolean autoStartup = true
+ ApplicationContext applicationContext
+ boolean waitForJobsToCompleteOnShutdown = false
+ int phase = Integer.MAX_VALUE
+ Properties quartzProperties
+ JobListener[] globalJobListeners
+
+ public void setApplicationContext(ApplicationContext applicationContext) {
+ this.applicationContext = applicationContext;
+ }
+
+ //SmartLifecycle
+
+
+ //---------------------------------------------------------------------
+ // Implementation of InitializingBean interface
+ //---------------------------------------------------------------------
+
+ public void afterPropertiesSet() throws Exception {
+ println "afterPropertiesSet Quartz Scheduler in QuartzFactoryBean"
+ if(quartzProperties){
+ StdSchedulerFactory fact = new StdSchedulerFactory();
+ fact.initialize(quartzProperties)
+ scheduler = fact.getScheduler()
+ }else{
+ scheduler = StdSchedulerFactory.getDefaultScheduler()
+ }
+
+ scheduler.getContext().put("applicationContext", this.applicationContext)
+ scheduler.getContext().put("grailsApplication", this.grailsApplication)
+ if (this.globalJobListeners != null) {
+ for (JobListener listener : this.globalJobListeners) {
+ scheduler.listenerManager.addJobListener(listener)
+ }
+ }
+ }
+
+ //---------------------------------------------------------------------
+ // Implementation of FactoryBean interface
+ //---------------------------------------------------------------------
+
+ @Override
+ public Scheduler getScheduler() {
+ return this.scheduler;
+ }
+
+ public Scheduler getObject() {
+ return this.scheduler;
+ }
+
+ public Class<? extends Scheduler> getObjectType() {
+ return (this.scheduler != null) ? this.scheduler.getClass() : Scheduler.class;
+ }
+
+ public boolean isSingleton() {
+ return true;
+ }
+
+
+ //---------------------------------------------------------------------
+ // Implementation of Lifecycle interface
+ //---------------------------------------------------------------------
+
+ public void start() throws SchedulingException {
+ if (this.scheduler != null) {
+ try {
+ log.info("Starting Quartz Scheduler in QuartzFactoryBean");
+ println "Starting Quartz Scheduler in QuartzFactoryBean"
+ scheduler.start();
+ }
+ catch (SchedulerException ex) {
+ throw new SchedulingException("Could not start Quartz Scheduler", ex);
+ }
+ }
+ }
+
+ public void stop() throws SchedulingException {
+ if (this.scheduler != null) {
+ try {
+ this.scheduler.standby();
+ }
+ catch (SchedulerException ex) {
+ throw new SchedulingException("Could not stop Quartz Scheduler", ex);
+ }
+ }
+ }
+
+ public void stop(Runnable callback) throws SchedulingException {
+ stop();
+ callback.run();
+ }
+
+ public boolean isRunning() throws SchedulingException {
+ if (this.scheduler != null) {
+ try {
+ return !this.scheduler.isInStandbyMode();
+ }
+ catch (SchedulerException ex) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ public boolean isAutoStartup() {
+ return this.autoStartup;
+ }
+
+ //---------------------------------------------------------------------
+ // Implementation of DisposableBean interface
+ //---------------------------------------------------------------------
+
+ /**
+ * Shut down the Quartz scheduler on bean factory shutdown,
+ * stopping all scheduled jobs.
+ */
+ public void destroy() throws SchedulerException {
+ log.info("Shutting down Quartz Scheduler");
+ this.scheduler.shutdown(this.waitForJobsToCompleteOnShutdown);
+ }
+
+}
44 src/groovy/grails/plugin/quartz2/QuartzHelper.groovy
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+package grails.plugin.quartz2;
+
+import org.quartz.DisallowConcurrentExecution;
+import org.quartz.Job;
+import org.quartz.JobBuilder;
+import org.quartz.JobDataMap;
+import org.quartz.JobDetail;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobKey;
+import org.quartz.utils.Key;
+import org.quartz.PersistJobDataAfterExecution;
+import org.quartz.Scheduler;
+import org.quartz.StatefulJob;
+import org.quartz.Trigger;
+import org.quartz.utils.ClassUtils;
+
+
+/**
+ * a set of statics to aid in building different jobs, job details and
+ *
+ * @author Joshua Burnett
+ */
+class QuartzHelper {
+
+ static JobDetail createJob(String name, Closure clos){
+ return null
+ }
+}
223 src/groovy/grails/plugin/quartz2/SimpleJobDetail.groovy
@@ -0,0 +1,223 @@
+/*
+ * 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.
+ */
+
+package grails.plugin.quartz2;
+
+import org.quartz.DisallowConcurrentExecution;
+import org.quartz.Job;
+import org.quartz.JobBuilder;
+import org.quartz.JobDataMap;
+import org.quartz.JobDetail;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobKey;
+import org.quartz.utils.Key;
+import org.quartz.PersistJobDataAfterExecution;
+import org.quartz.Scheduler;
+import org.quartz.StatefulJob;
+import org.quartz.Trigger;
+import org.quartz.utils.ClassUtils;
+
+
+/**
+ * a Groovy implementation of a JobDetail.
+ * allows the contructor to be used for name key and class.
+ * has a property for setDisallowConcurrentExecution so the job class does need to be tied to the annotaion
+ * makes it easier to put a list of maps in for the JobDataMap
+ *
+ * @author Joshua Burnett
+ */
+class SimpleJobDetail implements Cloneable, java.io.Serializable, JobDetail {
+
+ //String name = Key.createUniqueName(Scheduler.DEFAULT_GROUP)
+ //String group = Scheduler.DEFAULT_GROUP
+ String name
+ String group = Scheduler.DEFAULT_GROUP
+ String description
+ Class<? extends Job> jobClass
+ JobDataMap jobDataMap = new JobDataMap()
+ boolean durability = false
+ boolean shouldRecover = false
+ boolean concurrent = true
+ boolean persistJobDataAfterExecution = false
+ private transient JobKey key
+
+
+ SimpleJobDetail() {
+ }
+
+ SimpleJobDetail(String name, Class<? extends Job> jobClass) {
+ this.name = name
+ this.jobClass = jobClass
+ }
+
+ SimpleJobDetail(String name, Class<? extends Job> jobClass, Map dataMap) {
+ this.name = name
+ this.jobClass = jobClass
+ if(dataMap instanceof JobDataMap){
+ this.jobDataMap = dataMap
+ }else{
+ this.jobDataMap = new JobDataMap(dataMap)
+ }
+ }
+
+ String getFullName() {
+ return "${key.group}.${key.name}"
+ }
+
+ void setName(String name) {
+ if (!name || !name.trim() ) throw new IllegalArgumentException("Job name cannot be empty.");
+
+ this.name = name
+ this.key = null
+ }
+
+
+ void setGroup(String group) {
+ if (!group || !group.trim() ) {
+ group = Scheduler.DEFAULT_GROUP
+ }
+ this.group = group
+ this.key = null
+ }
+
+ JobKey getKey() {
+ if(!this.key) {
+ if(!name)
+ name = Key.createUniqueName(Scheduler.DEFAULT_GROUP)
+ this.key = new JobKey(name, group);
+ }
+
+ return this.key
+ }
+
+ public void setKey(JobKey jk) {
+ if(jk == null) throw new IllegalArgumentException("Key cannot be null!");
+
+ name = jk.name
+ group = jk.group
+ this.key = jk
+ }
+
+ /**
+ * puts a normal map into the jobDataMap
+ */
+ void setJobData(Map map){
+ @SuppressWarnings("unchecked") // casting to keep API compatible and avoid compiler errors/warnings.
+ Map<String, Object> mapTyped = (Map<String, Object>)map
+ jobDataMap.putAll(mapTyped)
+ }
+
+
+ /**
+ * @return whether the associated Job class carries the {@link PersistJobDataAfterExecution} annotation.
+ */
+ boolean isPersistJobDataAfterExecution() {
+ if(persistJobDataAfterExecution){
+ return true
+ }else{//try the annotation
+ return ClassUtils.isAnnotationPresent(jobClass, PersistJobDataAfterExecution.class);
+ }
+ }
+
+ /**
+ * @return whether the associated Job class carries the {@link DisallowConcurrentExecution} annotation.
+ */
+ boolean isConcurrentExectionDisallowed() {
+ if(!concurrent){
+ return true
+ } else{
+ //try the annotation if concurrent is true just to give it a shot and stick with quartz standard
+ return ClassUtils.isAnnotationPresent(jobClass, DisallowConcurrentExecution.class);
+ }
+
+ }
+
+ /**
+ * @return whether the associated Job class carries the {@link DisallowConcurrentExecution} annotation.
+ */
+ void setConcurrentExectionDisallowed(boolean singleThreaded) {
+ concurrent = !singleThreaded
+ }
+
+ boolean requestsRecovery() {
+ return shouldRecover
+ }
+
+ void setRequestsRecovery(boolean shouldRecover) {
+ this.shouldRecover = shouldRecover
+ }
+
+ void setDurability(boolean durability) {
+ this.durability = durability;
+ }
+
+ public boolean isDurable() {
+ return durability;
+ }
+
+ @Override
+ String toString() {
+ return "JobDetail '${key.group}.${key.name}' : jobClass: '${jobClass?.name}'"
+ + " concurrentExectionDisallowed: " + isConcurrentExectionDisallowed()
+ + " persistJobDataAfterExecution: " + isPersistJobDataAfterExecution()
+ + " isDurable: " + isDurable() + " requestsRecovers: " + requestsRecovery();
+ }
+
+ @Override
+ boolean equals(Object obj) {
+ if (!(obj instanceof JobDetail)) {
+ return false;
+ }
+ JobDetail other = (JobDetail) obj;
+ if(other.getKey() == null || getKey() == null)
+ return false;
+ if (!other.getKey().equals(getKey())) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ int hashCode() {
+ return getKey().hashCode();
+ }
+
+ @Override
+ Object clone() {
+ SimpleJobDetail copy;
+ try {
+ copy = (SimpleJobDetail) super.clone();
+ if (jobDataMap != null) {
+ copy.jobDataMap = (JobDataMap) jobDataMap.clone();
+ }
+ } catch (CloneNotSupportedException ex) {
+ throw new IncompatibleClassChangeError("Not Cloneable.");
+ }
+ return copy;
+ }
+
+ //THIS IS KIND OF FUBAR
+ JobBuilder getJobBuilder() {
+ JobBuilder b = JobBuilder.newJob()
+ .ofType(getJobClass())
+ .requestRecovery(requestsRecovery())
+ .storeDurably(isDurable())
+ .usingJobData(getJobDataMap())
+ .withDescription(getDescription())
+ .withIdentity(getKey());
+ return b;
+ }
+}
132 test/projects/qkiss/app-qkiss-quartz.groovy
@@ -0,0 +1,132 @@
+import static org.quartz.JobBuilder.*;
+import static org.quartz.SimpleScheduleBuilder.*;
+import static org.quartz.CronScheduleBuilder.*;
+import static org.quartz.CalendarIntervalScheduleBuilder.*;
+import static org.quartz.TriggerBuilder.*;
+import static org.quartz.DateBuilder.*;
+import grails.plugin.quartz2.InvokeMethodJob
+import grails.plugin.quartz2.SimpleJobDetail
+import grails.plugin.quartz2.ClosureJob
+import org.quartz.*
+import org.quartz.impl.triggers.SimpleTriggerImpl
+import qkiss.*
+
+grails.plugins.quartz.autoStartup = true
+
+org{
+ quartz{
+ scheduler.instanceName = 'TestAppScheduler'
+ threadPool.class = 'org.quartz.simpl.SimpleThreadPool'
+ threadPool.threadCount = 5
+ threadPool.threadPriority = 5
+ threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
+ jobStore.class = 'org.quartz.simpl.RAMJobStore'
+ jobStore.misfireThreshold =60000
+ }
+}
+
+grails.plugin.quartz2.jobSetup.job2 = { quartzScheduler, ctx ->
+ def org = new Org(name:'external')
+ Org.withTransaction{
+ org.save()
+ }
+
+ // define the job and tie it to our HelloJob class
+ def jobDet = new SimpleJobDetail("hello from external",HelloFromExternalConfigJob.class, [orgId:org.id] )
+ jobDet.concurrentExectionDisallowed = true
+ assert jobDet.isConcurrentExectionDisallowed()
+
+ Trigger trigger = newTrigger().withIdentity("HelloFromExternalConfigJobTrigger")
+ .withSchedule(
+ simpleSchedule()
+ .withIntervalInSeconds(1)
+ .withRepeatCount(2)
+ .withMisfireHandlingInstructionNextWithExistingCount()
+ )
+ .startNow()
+ .build()
+
+ quartzScheduler.scheduleJob(jobDet, trigger)
+
+
+
+ //test the static call
+ def dmap = new JobDataMap([targetClass:HunterService,targetMethod:'Buy_The_Ticket'])
+ JobDetail job3 = newJob(InvokeMethodJob.class).withIdentity("buy the ticket")
+ .usingJobData(dmap)
+ .build()
+ assert job3.isConcurrentExectionDisallowed() == false //I just wanted to verify that my metaClass overrid e above didn't stick
+
+ Trigger trigger3 = newTrigger().withIdentity("hunter trigger1")
+ .withSchedule(simpleSchedule().withIntervalInSeconds(4).repeatForever())
+ .startNow().build()
+
+ quartzScheduler.scheduleJob(job3, trigger3)
+
+ //test a service call
+ def hunterService = ctx.hunterService
+ def dmap2 = new JobDataMap([targetObject:hunterService,targetMethod:'takeTheRide',arguments:[true]])
+ JobDetail job2 = newJob(InvokeMethodJob.class).withIdentity("take the ride")
+ .usingJobData(dmap2).build()
+
+ Trigger trigger2 = newTrigger().withIdentity("hunter trigger2")
+ .withSchedule(simpleSchedule().withIntervalInSeconds(4).repeatForever())
+ .startNow().build()
+
+ quartzScheduler.scheduleJob(job2, trigger2)
+
+ def cj = ClosureJob.createJob { jobCtx , appCtx->
+ appCtx.hunterService.takeTheRide(true)
+ println "ClosureJob"
+ }
+
+ def ctrig = new SimpleTriggerImpl(name:"trig1", startTime:new Date(),repeatInterval:1000,repeatCount:-1)
+
+ quartzScheduler.scheduleJob(cj, ctrig)
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/********Future builder? spec, not sure its worth it***********/
+grails.plugin.quartz2.schedule.jobDetails=[
+ cleverJob:[
+ group:"someGroup", //(optional), sets the group int he JobKey
+ description:"does something clever",
+ jobClass:'grails.plugins.quartz2.SpringBeanMethodJob',
+ jobDataMap:[beanName:'someService',beanMethod:"goBabyGo",methodParams:[1,2,3]],
+ ],
+ anotherCleverJob:[
+ group:'mainGroup',
+ description:"does something clever",
+ jobClosure:{ appCtx, jobCtx ->
+ def org = ArDoc.get(1)
+ if(arDoc) appCtx.outApiService.runOutApi(ardoc)
+ }
+ ]
+]//closes jobdetails list
+
+
+grails.plugin.quartz2.schedule.triggers=[
+ cleverJobTrigger:[
+ triggerType:'cron', //can be cron,simple,custom
+ startDelay:10000, //from here down its just property settings on the
+ cronExpression: '0/6 * 15 * * ?'
+ ]
+]
+
8 test/projects/qkiss/application.properties
@@ -0,0 +1,8 @@
+#Grails Metadata file
+#Thu Oct 06 14:30:37 CDT 2011
+app.grails.version=1.3.7
+app.name=qkiss
+app.servlet.version=2.4
+app.version=0.1
+plugins.hibernate=1.3.7
+plugins.tomcat=1.3.7
38 test/projects/qkiss/grails-app/conf/BootStrap.groovy
@@ -0,0 +1,38 @@
+import static org.quartz.JobBuilder.*;
+import static org.quartz.SimpleScheduleBuilder.*;
+import static org.quartz.CronScheduleBuilder.*;
+import static org.quartz.CalendarIntervalScheduleBuilder.*;
+import static org.quartz.TriggerBuilder.*;
+import static org.quartz.DateBuilder.*;
+import org.quartz.JobDetail
+import org.quartz.Trigger
+
+class BootStrap {
+ def grailsApplication
+ def quartzScheduler
+
+ def init = { servletContext ->
+ // define the job and tie it to our HelloJob class
+/* JobDetail job = newJob(HelloJob.class)
+ .withIdentity("JobTest")
+ .usingJobData("jobDetailName",'BootStrap')
+ .build()
+
+ // Trigger the job to run now, and then every 40 seconds
+ Trigger trigger = newTrigger()
+ .withIdentity("myTrigger")
+ .withSchedule(simpleSchedule()
+ .withIntervalInSeconds(4)
+ .repeatForever())
+ .startNow()
+ .build()
+
+ // Tell quartz to schedule the job using our trigger
+ quartzScheduler.scheduleJob(job, trigger);*/
+
+
+ }
+ def destroy = {
+
+ }
+}
32 test/projects/qkiss/grails-app/conf/BuildConfig.groovy
@@ -0,0 +1,32 @@
+grails.plugin.location.'quartz-scheduler' ="../../.."
+grails.project.class.dir = "target/classes"
+grails.project.test.class.dir = "target/test-classes"
+grails.project.test.reports.dir = "target/test-reports"
+//grails.project.war.file = "target/${appName}-${appVersion}.war"
+grails.project.dependency.resolution = {
+ // inherit Grails' default dependencies
+ inherits("global") {
+ // uncomment to disable ehcache
+ // excludes 'ehcache'
+ }
+ log "warn" // log level of Ivy resolver, either 'error', 'warn', 'info', 'debug' or 'verbose'
+ repositories {
+ grailsPlugins()
+ grailsHome()
+ grailsCentral()
+
+ // uncomment the below to enable remote dependency resolution
+ // from public Maven repositories
+ //mavenLocal()
+ //mavenCentral()
+ //mavenRepo "http://snapshots.repository.codehaus.org"
+ //mavenRepo "http://repository.codehaus.org"
+ //mavenRepo "http://download.java.net/maven/2/"
+ //mavenRepo "http://repository.jboss.com/maven2/"
+ }
+ dependencies {
+ // specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes eg.
+
+ // runtime 'mysql:mysql-connector-java:5.1.13'
+ }
+}
60 test/projects/qkiss/grails-app/conf/Config.groovy
@@ -0,0 +1,60 @@
+import static org.quartz.JobBuilder.*;
+import static org.quartz.SimpleScheduleBuilder.*;
+import static org.quartz.CronScheduleBuilder.*;
+import static org.quartz.CalendarIntervalScheduleBuilder.*;
+import static org.quartz.TriggerBuilder.*;
+import static org.quartz.DateBuilder.*;
+import org.quartz.*
+import qkiss.*
+
+grails.config.locations = [ "file:app-${appName}-quartz.groovy"]
+// "classpath:${appName}-config.groovy"
+
+// log4j configuration
+log4j = {
+ root { info() }
+
+ appenders {
+ console name:'stdout', layout:pattern(conversionPattern: '%d{HH:mm:ss,SSS} [%t] %-5p %c %x - %m%n')
+ }
+
+ error 'org.codehaus.groovy.grails.web.servlet', // controllers
+ 'org.codehaus.groovy.grails.web.pages', // GSP
+ 'org.codehaus.groovy.grails.web.sitemesh', // layouts
+ 'org.codehaus.groovy.grails.web.mapping.filter', // URL mapping
+ 'org.codehaus.groovy.grails.web.mapping', // URL mapping
+ 'org.codehaus.groovy.grails.commons', // core / classloading
+ 'org.codehaus.groovy.grails.plugins', // plugins
+ 'org.codehaus.groovy.grails.orm.hibernate', // hibernate integration
+ 'org.springframework',
+ 'org.hibernate',
+ 'net.sf.ehcache.hibernate'
+
+ warn 'org.mortbay.log'
+ debug 'qkiss'
+}
+
+grails.plugin.quartz2.jobSetup.job1 = { quartzScheduler, ctx ->
+ def org = new Org(name:'skydive')
+ Org.withTransaction{ //inside session
+ org.save()
+ }
+ // define the job and tie it to our HelloJob class
+ JobDetail job = newJob(HelloJob.class)
+ .withIdentity("configTest")
+ .usingJobData(new JobDataMap([jobDetailName:'mainConfig']) )
+ .build()
+
+ // Trigger the job to run now, and then every 40 seconds
+ Trigger trigger = newTrigger()
+ .withIdentity("configTestTrigger")
+ .withSchedule(simpleSchedule()
+ .withIntervalInSeconds(5)
+ .repeatForever())
+ .startNow()
+ .build()
+
+ // Tell quartz to schedule the job using our trigger
+ quartzScheduler.scheduleJob(job, trigger);
+}
+
32 test/projects/qkiss/grails-app/conf/DataSource.groovy
@@ -0,0 +1,32 @@
+dataSource {
+ pooled = true
+ driverClassName = "org.hsqldb.jdbcDriver"
+ username = "sa"
+ password = ""
+}
+hibernate {
+ cache.use_second_level_cache = true
+ cache.use_query_cache = true
+ cache.provider_class = 'net.sf.ehcache.hibernate.EhCacheProvider'
+}
+// environment specific settings
+environments {
+ development {
+ dataSource {
+ dbCreate = "create-drop" // one of 'create', 'create-drop','update'
+ url = "jdbc:hsqldb:mem:devDB"
+ }
+ }
+ test {
+ dataSource {
+ dbCreate = "update"
+ url = "jdbc:hsqldb:mem:testDb"
+ }
+ }
+ production {
+ dataSource {
+ dbCreate = "update"
+ url = "jdbc:hsqldb:file:prodDb;shutdown=true"
+ }
+ }
+}
13 test/projects/qkiss/grails-app/conf/UrlMappings.groovy
@@ -0,0 +1,13 @@
+class UrlMappings {
+
+ static mappings = {
+ "/$controller/$action?/$id?"{
+ constraints {
+ // apply constraints here
+ }
+ }
+
+ "/"(view:"/index")
+ "500"(view:'/error')
+ }
+}
3  test/projects/qkiss/grails-app/conf/spring/resources.groovy
@@ -0,0 +1,3 @@
+// Place your Spring DSL code here
+beans = {
+}
9 test/projects/qkiss/grails-app/domain/qkiss/Org.groovy
@@ -0,0 +1,9 @@
+package qkiss
+
+class Org {
+
+ String name
+
+ static constraints = {
+ }
+}
55 test/projects/qkiss/grails-app/i18n/messages.properties
@@ -0,0 +1,55 @@
+default.doesnt.match.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}]
+default.invalid.url.message=Property [{0}] of class [{1}] with value [{2}] is not a valid URL
+default.invalid.creditCard.message=Property [{0}] of class [{1}] with value [{2}] is not a valid credit card number
+default.invalid.email.message=Property [{0}] of class [{1}] with value [{2}] is not a valid e-mail address
+default.invalid.range.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid range from [{3}] to [{4}]
+default.invalid.size.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid size range from [{3}] to [{4}]
+default.invalid.max.message=Property [{0}] of class [{1}] with value [{2}] exceeds maximum value [{3}]
+default.invalid.min.message=Property [{0}] of class [{1}] with value [{2}] is less than minimum value [{3}]
+default.invalid.max.size.message=Property [{0}] of class [{1}] with value [{2}] exceeds the maximum size of [{3}]
+default.invalid.min.size.message=Property [{0}] of class [{1}] with value [{2}] is less than the minimum size of [{3}]
+default.invalid.validator.message=Property [{0}] of class [{1}] with value [{2}] does not pass custom validation
+default.not.inlist.message=Property [{0}] of class [{1}] with value [{2}] is not contained within the list [{3}]
+default.blank.message=Property [{0}] of class [{1}] cannot be blank
+default.not.equal.message=Property [{0}] of class [{1}] with value [{2}] cannot equal [{3}]
+default.null.message=Property [{0}] of class [{1}] cannot be null
+default.not.unique.message=Property [{0}] of class [{1}] with value [{2}] must be unique
+
+default.paginate.prev=Previous
+default.paginate.next=Next
+default.boolean.true=True
+default.boolean.false=False
+default.date.format=yyyy-MM-dd HH:mm:ss z
+default.number.format=0
+
+default.created.message={0} {1} created
+default.updated.message={0} {1} updated
+default.deleted.message={0} {1} deleted
+default.not.deleted.message={0} {1} could not be deleted
+default.not.found.message={0} not found with id {1}
+default.optimistic.locking.failure=Another user has updated this {0} while you were editing
+
+default.home.label=Home
+default.list.label={0} List
+default.add.label=Add {0}
+default.new.label=New {0}
+default.create.label=Create {0}
+default.show.label=Show {0}
+default.edit.label=Edit {0}
+
+default.button.create.label=Create
+default.button.edit.label=Edit
+default.button.update.label=Update
+default.button.delete.label=Delete
+default.button.delete.confirm.message=Are you sure?
+
+# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=Property {0} must be a valid URL
+typeMismatch.java.net.URI=Property {0} must be a valid URI
+typeMismatch.java.util.Date=Property {0} must be a valid Date
+typeMismatch.java.lang.Double=Property {0} must be a valid number
+typeMismatch.java.lang.Integer=Property {0} must be a valid number
+typeMismatch.java.lang.Long=Property {0} must be a valid number
+typeMismatch.java.lang.Short=Property {0} must be a valid number
+typeMismatch.java.math.BigDecimal=Property {0} must be a valid number
+typeMismatch.java.math.BigInteger=Property {0} must be a valid number
56 test/projects/qkiss/grails-app/i18n/messages_da.properties
@@ -0,0 +1,56 @@
+default.doesnt.match.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] overholder ikke mønsteret [{3}]
+default.invalid.url.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] er ikke en gyldig URL
+default.invalid.creditCard.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] er ikke et gyldigt kreditkortnummer
+default.invalid.email.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] er ikke en gyldig e-mail adresse
+default.invalid.range.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] ligger ikke inden for intervallet fra [{3}] til [{4}]
+default.invalid.size.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] ligger ikke inden for størrelsen fra [{3}] til [{4}]
+default.invalid.max.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] overstiger den maksimale værdi [{3}]
+default.invalid.min.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] er under den minimale værdi [{3}]
+default.invalid.max.size.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] overstiger den maksimale størrelse på [{3}]
+default.invalid.min.size.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] er under den minimale størrelse på [{3}]
+default.invalid.validator.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] overholder ikke den brugerdefinerede validering
+default.not.inlist.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] findes ikke i listen [{3}]
+default.blank.message=Feltet [{0}] i klassen [{1}] kan ikke være tom
+default.not.equal.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] må ikke være [{3}]
+default.null.message=Feltet [{0}] i klassen [{1}] kan ikke være null
+default.not.unique.message=Feltet [{0}] i klassen [{1}] som har værdien [{2}] skal være unik
+
+default.paginate.prev=Forrige
+default.paginate.next=Næste
+default.boolean.true=Sand
+default.boolean.false=Falsk
+default.date.format=yyyy-MM-dd HH:mm:ss z
+default.number.format=0
+
+default.created.message={0} {1} oprettet
+default.updated.message={0} {1} opdateret
+default.deleted.message={0} {1} slettet
+default.not.deleted.message={0} {1} kunne ikke slettes
+default.not.found.message={0} med id {1} er ikke fundet
+default.optimistic.locking.failure=En anden bruger har opdateret denne {0} imens du har lavet rettelser
+
+default.home.label=Hjem
+default.list.label={0} Liste
+default.add.label=Tilføj {0}
+default.new.label=Ny {0}
+default.create.label=Opret {0}
+default.show.label=Vis {0}
+default.edit.label=Ret {0}
+
+default.button.create.label=Opret
+default.button.edit.label=Ret
+default.button.update.label=Opdater
+default.button.delete.label=Slet
+default.button.delete.confirm.message=Er du sikker?
+
+# Databindingsfejl. Brug "typeMismatch.$className.$propertyName for at passe til en given klasse (f.eks typeMismatch.Book.author)
+typeMismatch.java.net.URL=Feltet {0} skal være en valid URL
+typeMismatch.java.net.URI=Feltet {0} skal være en valid URI
+typeMismatch.java.util.Date=Feltet {0} skal være en valid Dato
+typeMismatch.java.lang.Double=Feltet {0} skal være et valid tal
+typeMismatch.java.lang.Integer=Feltet {0} skal være et valid tal
+typeMismatch.java.lang.Long=Feltet {0} skal være et valid tal
+typeMismatch.java.lang.Short=Feltet {0} skal være et valid tal
+typeMismatch.java.math.BigDecimal=Feltet {0} skal være et valid tal
+typeMismatch.java.math.BigInteger=Feltet {0} skal være et valid tal
+
55 test/projects/qkiss/grails-app/i18n/messages_de.properties
@@ -0,0 +1,55 @@
+default.doesnt.match.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] entspricht nicht dem vorgegebenen Muster [{3}]
+default.invalid.url.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist keine gültige URL
+default.invalid.creditCard.message=Das Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist keine gültige Kreditkartennummer
+default.invalid.email.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist keine gültige E-Mail Adresse
+default.invalid.range.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist nicht im Wertebereich von [{3}] bis [{4}]
+default.invalid.size.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist nicht im Wertebereich von [{3}] bis [{4}]
+default.invalid.max.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist größer als der Höchstwert von [{3}]
+default.invalid.min.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist kleiner als der Mindestwert von [{3}]
+default.invalid.max.size.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] übersteigt den Höchstwert von [{3}]
+default.invalid.min.size.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] unterschreitet den Mindestwert von [{3}]
+default.invalid.validator.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist ungültig
+default.not.inlist.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] ist nicht in der Liste [{3}] enthalten.
+default.blank.message=Die Eigenschaft [{0}] des Typs [{1}] darf nicht leer sein
+default.not.equal.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] darf nicht gleich [{3}] sein
+default.null.message=Die Eigenschaft [{0}] des Typs [{1}] darf nicht null sein
+default.not.unique.message=Die Eigenschaft [{0}] des Typs [{1}] mit dem Wert [{2}] darf nur einmal vorkommen
+
+default.paginate.prev=Vorherige
+default.paginate.next=Nächste
+default.boolean.true=Wahr
+default.boolean.false=Falsch
+default.date.format=dd.MM.yyyy HH:mm:ss z
+default.number.format=0
+
+default.created.message={0} {1} wurde angelegt
+default.updated.message={0} {1} wurde geändert
+default.deleted.message={0} {1} wurde gelöscht
+default.not.deleted.message={0} {1} konnte nicht gelöscht werden
+default.not.found.message={0} mit der id {1} wurde nicht gefunden
+default.optimistic.locking.failure=Ein anderer Benutzer hat das {0} Object geändert während Sie es bearbeitet haben
+
+default.home.label=Home
+default.list.label={0} Liste
+default.add.label={0} hinzufügen
+default.new.label={0} anlegen
+default.create.label={0} anlegen
+default.show.label={0} anzeigen
+default.edit.label={0} bearbeiten
+
+default.button.create.label=Anlegen
+default.button.edit.label=Bearbeiten
+default.button.update.label=Aktualisieren
+default.button.delete.label=Löschen
+default.button.delete.confirm.message=Sind Sie sicher?
+
+# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=Die Eigenschaft {0} muss eine gültige URL sein
+typeMismatch.java.net.URI=Die Eigenschaft {0} muss eine gültige URI sein
+typeMismatch.java.util.Date=Die Eigenschaft {0} muss ein gültiges Datum sein
+typeMismatch.java.lang.Double=Die Eigenschaft {0} muss eine gültige Zahl sein
+typeMismatch.java.lang.Integer=Die Eigenschaft {0} muss eine gültige Zahl sein
+typeMismatch.java.lang.Long=Die Eigenschaft {0} muss eine gültige Zahl sein
+typeMismatch.java.lang.Short=Die Eigenschaft {0} muss eine gültige Zahl sein
+typeMismatch.java.math.BigDecimal=Die Eigenschaft {0} muss eine gültige Zahl sein
+typeMismatch.java.math.BigInteger=Die Eigenschaft {0} muss eine gültige Zahl sein
30 test/projects/qkiss/grails-app/i18n/messages_es.properties
@@ -0,0 +1,30 @@
+default.doesnt.match.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no corresponde al patrón [{3}]
+default.invalid.url.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no es una URL válida
+default.invalid.creditCard.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no es un número de tarjeta de crédito válida
+default.invalid.email.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no es una dirección de correo electrónico válida
+default.invalid.range.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no entra en el rango válido de [{3}] a [{4}]
+default.invalid.size.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no entra en el tamaño válido de [{3}] a [{4}]
+default.invalid.max.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] excede el valor máximo [{3}]
+default.invalid.min.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] es menos que el valor mínimo [{3}]
+default.invalid.max.size.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] excede el tamaño máximo de [{3}]
+default.invalid.min.size.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] es menor que el tamaño mínimo de [{3}]
+default.invalid.validator.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no es válido
+default.not.inlist.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no esta contenido dentro de la lista [{3}]
+default.blank.message=La propiedad [{0}] de la clase [{1}] no puede ser vacía
+default.not.equal.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] no puede igualar a [{3}]
+default.null.message=La propiedad [{0}] de la clase [{1}] no puede ser nulo
+default.not.unique.message=La propiedad [{0}] de la clase [{1}] con valor [{2}] debe ser única
+
+default.paginate.prev=Anterior
+default.paginate.next=Siguiente
+
+# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=La propiedad {0} debe ser una URL válida
+typeMismatch.java.net.URI=La propiedad {0} debe ser una URI válida
+typeMismatch.java.util.Date=La propiedad {0} debe ser una fecha válida
+typeMismatch.java.lang.Double=La propiedad {0} debe ser un número válido
+typeMismatch.java.lang.Integer=La propiedad {0} debe ser un número válido
+typeMismatch.java.lang.Long=La propiedad {0} debe ser un número válido
+typeMismatch.java.lang.Short=La propiedad {0} debe ser un número válido
+typeMismatch.java.math.BigDecimal=La propiedad {0} debe ser un número válido
+typeMismatch.java.math.BigInteger=La propiedad {0} debe ser un número válido
19 test/projects/qkiss/grails-app/i18n/messages_fr.properties
@@ -0,0 +1,19 @@
+default.doesnt.match.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] ne correspond pas au pattern [{3}]
+default.invalid.url.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas une URL valide
+default.invalid.creditCard.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas un numéro de carte de crédit valide
+default.invalid.email.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas une adresse e-mail valide
+default.invalid.range.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas contenue dans l'intervalle [{3}] à [{4}]
+default.invalid.size.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas contenue dans l'intervalle [{3}] à [{4}]
+default.invalid.max.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] est supérieure à la valeur maximum [{3}]
+default.invalid.min.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] est inférieure à la valeur minimum [{3}]
+default.invalid.max.size.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] est supérieure à la valeur maximum [{3}]
+default.invalid.min.size.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] est inférieure à la valeur minimum [{3}]
+default.invalid.validator.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] n'est pas valide
+default.not.inlist.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] ne fait pas partie de la liste [{3}]
+default.blank.message=La propriété [{0}] de la classe [{1}] ne peut pas être vide
+default.not.equal.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] ne peut pas être égale à [{3}]
+default.null.message=La propriété [{0}] de la classe [{1}] ne peut pas être nulle
+default.not.unique.message=La propriété [{0}] de la classe [{1}] avec la valeur [{2}] doit être unique
+
+default.paginate.prev=Précédent
+default.paginate.next=Suivant
19 test/projects/qkiss/grails-app/i18n/messages_it.properties
@@ -0,0 +1,19 @@
+default.doesnt.match.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non corrisponde al pattern [{3}]
+default.invalid.url.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non è un URL valido
+default.invalid.creditCard.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non è un numero di carta di credito valido
+default.invalid.email.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non è un indirizzo email valido
+default.invalid.range.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non rientra nell'intervallo valido da [{3}] a [{4}]
+default.invalid.size.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non rientra nell'intervallo di dimensioni valide da [{3}] a [{4}]
+default.invalid.max.message=La proprietà [{0}] della classe [{1}] con valore [{2}] è maggiore di [{3}]
+default.invalid.min.message=La proprietà [{0}] della classe [{1}] con valore [{2}] è minore di [{3}]
+default.invalid.max.size.message=La proprietà [{0}] della classe [{1}] con valore [{2}] è maggiore di [{3}]
+default.invalid.min.size.message=La proprietà [{0}] della classe [{1}] con valore [{2}] è minore di [{3}]
+default.invalid.validator.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non è valida
+default.not.inlist.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non è contenuta nella lista [{3}]
+default.blank.message=La proprietà [{0}] della classe [{1}] non può essere vuota
+default.not.equal.message=La proprietà [{0}] della classe [{1}] con valore [{2}] non può essere uguale a [{3}]
+default.null.message=La proprietà [{0}] della classe [{1}] non può essere null
+default.not.unique.message=La proprietà [{0}] della classe [{1}] con valore [{2}] deve essere unica
+
+default.paginate.prev=Precedente
+default.paginate.next=Successivo
19 test/projects/qkiss/grails-app/i18n/messages_ja.properties
@@ -0,0 +1,19 @@
+default.doesnt.match.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]パターンと一致していません。
+default.invalid.url.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、URLではありません。
+default.invalid.creditCard.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、正当なクレジットカード番号ではありません。
+default.invalid.email.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、メールアドレスではありません。
+default.invalid.range.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]から[{4}]範囲内を指定してください。
+default.invalid.size.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]から[{4}]以内を指定してください。
+default.invalid.max.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、最大値[{3}]より大きいです。
+default.invalid.min.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、最小値[{3}]より小さいです。
+default.invalid.max.size.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、最大値[{3}]より大きいです。
+default.invalid.min.size.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、最小値[{3}]より小さいです。
+default.invalid.validator.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、カスタムバリデーションを通過できません。
+default.not.inlist.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]リスト内に存在しません。
+default.blank.message=[{1}]クラスのプロパティ[{0}]の空白は許可されません。
+default.not.equal.message=クラス[{1}]プロパティ[{0}]の値[{2}]は、[{3}]と同等ではありません。
+default.null.message=[{1}]クラスのプロパティ[{0}]にnullは許可されません。
+default.not.unique.message=クラス[{1}]プロパティ[{0}]の値[{2}]は既に使用されています。
+
+default.paginate.prev=戻る
+default.paginate.next=次へ
55 test/projects/qkiss/grails-app/i18n/messages_nl.properties
@@ -0,0 +1,55 @@
+default.doesnt.match.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] komt niet overeen met het vereiste patroon [{3}]
+default.invalid.url.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is geen geldige URL
+default.invalid.creditCard.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is geen geldig credit card nummer
+default.invalid.email.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is geen geldig e-mailadres
+default.invalid.range.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] valt niet in de geldige waardenreeks van [{3}] tot [{4}]
+default.invalid.size.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] valt niet in de geldige grootte van [{3}] tot [{4}]
+default.invalid.max.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] overschrijdt de maximumwaarde [{3}]
+default.invalid.min.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is minder dan de minimumwaarde [{3}]
+default.invalid.max.size.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] overschrijdt de maximumgrootte van [{3}]
+default.invalid.min.size.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is minder dan minimumgrootte van [{3}]
+default.invalid.validator.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] is niet geldig
+default.not.inlist.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] komt niet voor in de lijst [{3}]
+default.blank.message=Attribuut [{0}] van entiteit [{1}] mag niet leeg zijn
+default.not.equal.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] mag niet gelijk zijn aan [{3}]
+default.null.message=Attribuut [{0}] van entiteit [{1}] mag niet leeg zijn
+default.not.unique.message=Attribuut [{0}] van entiteit [{1}] met waarde [{2}] moet uniek zijn
+
+default.paginate.prev=Vorige
+default.paginate.next=Volgende
+default.boolean.true=Ja
+default.boolean.false=Nee
+default.date.format=dd-MM-yyyy HH:mm:ss z
+default.number.format=0
+
+default.created.message={0} {1} ingevoerd
+default.updated.message={0} {1} gewijzigd
+default.deleted.message={0} {1} verwijderd
+default.not.deleted.message={0} {1} kon niet worden verwijderd
+default.not.found.message={0} met id {1} kon niet worden gevonden
+default.optimistic.locking.failure=Een andere gebruiker heeft deze {0} al gewijzigd
+
+default.home.label=Home
+default.list.label={0} Overzicht
+default.add.label=Toevoegen {0}
+default.new.label=Invoeren {0}
+default.create.label=Invoeren {0}
+default.show.label=Details {0}
+default.edit.label=Wijzigen {0}
+
+default.button.create.label=Invoeren
+default.button.edit.label=Wijzigen
+default.button.update.label=Opslaan
+default.button.delete.label=Verwijderen
+default.button.delete.confirm.message=Weet je het zeker?
+
+# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=Attribuut {0} is geen geldige URL
+typeMismatch.java.net.URI=Attribuut {0} is geen geldige URI
+typeMismatch.java.util.Date=Attribuut {0} is geen geldige datum
+typeMismatch.java.lang.Double=Attribuut {0} is geen geldig nummer
+typeMismatch.java.lang.Integer=Attribuut {0} is geen geldig nummer
+typeMismatch.java.lang.Long=Attribuut {0} is geen geldig nummer
+typeMismatch.java.lang.Short=Attribuut {0} is geen geldig nummer
+typeMismatch.java.math.BigDecimal=Attribuut {0} is geen geldig nummer
+typeMismatch.java.math.BigInteger=Attribuut {0} is geen geldig nummer
34 test/projects/qkiss/grails-app/i18n/messages_pt_BR.properties
@@ -0,0 +1,34 @@
+#
+# Translated by Lucas Teixeira - lucastex@gmail.com
+#
+
+default.doesnt.match.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atende ao padrão definido [{3}]
+default.invalid.url.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é uma URL válida
+default.invalid.creditCard.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um número válido de cartão de crédito
+default.invalid.email.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um endereço de email válido.
+default.invalid.range.message=O campo [{0}] da classe [{1}] com o valor [{2}] não está entre a faixa de valores válida de [{3}] até [{4}]
+default.invalid.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] não está na faixa de tamanho válida de [{3}] até [{4}]
+default.invalid.max.message=O campo [{0}] da classe [{1}] com o valor [{2}] ultrapass o valor máximo [{3}]
+default.invalid.min.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atinge o valor mínimo [{3}]
+default.invalid.max.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] ultrapassa o tamanho máximo de [{3}]
+default.invalid.min.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atinge o tamanho mínimo de [{3}]
+default.invalid.validator.message=O campo [{0}] da classe [{1}] com o valor [{2}] não passou na validação
+default.not.inlist.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um valor dentre os permitidos na lista [{3}]
+default.blank.message=O campo [{0}] da classe [{1}] não pode ficar em branco
+default.not.equal.message=O campo [{0}] da classe [{1}] com o valor [{2}] não pode ser igual a [{3}]
+default.null.message=O campo [{0}] da classe [{1}] não pode ser vazia
+default.not.unique.message=O campo [{0}] da classe [{1}] com o valor [{2}] deve ser único
+
+default.paginate.prev=Anterior
+default.paginate.next=Próximo
+
+# Mensagens de erro em atribuição de valores. Use "typeMismatch.$className.$propertyName" para customizar (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=O campo {0} deve ser uma URL válida.
+typeMismatch.java.net.URI=O campo {0} deve ser uma URI válida.
+typeMismatch.java.util.Date=O campo {0} deve ser uma data válida
+typeMismatch.java.lang.Double=O campo {0} deve ser um número válido.
+typeMismatch.java.lang.Integer=O campo {0} deve ser um número válido.
+typeMismatch.java.lang.Long=O campo {0} deve ser um número válido.
+typeMismatch.java.lang.Short=O campo {0} deve ser um número válido.
+typeMismatch.java.math.BigDecimal=O campo {0} deve ser um número válido.
+typeMismatch.java.math.BigInteger=O campo {0} deve ser um número válido.
34 test/projects/qkiss/grails-app/i18n/messages_pt_PT.properties
@@ -0,0 +1,34 @@
+#
+# translation by miguel.ping@gmail.com, based on pt_BR translation by Lucas Teixeira - lucastex@gmail.com
+#
+
+default.doesnt.match.message=O campo [{0}] da classe [{1}] com o valor [{2}] não corresponde ao padrão definido [{3}]
+default.invalid.url.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um URL válido
+default.invalid.creditCard.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um número válido de cartão de crédito
+default.invalid.email.message=O campo [{0}] da classe [{1}] com o valor [{2}] não é um endereço de email válido.
+default.invalid.range.message=O campo [{0}] da classe [{1}] com o valor [{2}] não está dentro dos limites de valores válidos de [{3}] a [{4}]
+default.invalid.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] está fora dos limites de tamanho válido de [{3}] a [{4}]
+default.invalid.max.message=O campo [{0}] da classe [{1}] com o valor [{2}] ultrapassa o valor máximo [{3}]
+default.invalid.min.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atinge o valor mínimo [{3}]
+default.invalid.max.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] ultrapassa o tamanho máximo de [{3}]
+default.invalid.min.size.message=O campo [{0}] da classe [{1}] com o valor [{2}] não atinge o tamanho mínimo de [{3}]
+default.invalid.validator.message=O campo [{0}] da classe [{1}] com o valor [{2}] não passou na validação
+default.not.inlist.message=O campo [{0}] da classe [{1}] com o valor [{2}] não se encontra nos valores permitidos da lista [{3}]
+default.blank.message=O campo [{0}] da classe [{1}] não pode ser vazio
+default.not.equal.message=O campo [{0}] da classe [{1}] com o valor [{2}] não pode ser igual a [{3}]
+default.null.message=O campo [{0}] da classe [{1}] não pode ser vazio
+default.not.unique.message=O campo [{0}] da classe [{1}] com o valor [{2}] deve ser único
+
+default.paginate.prev=Anterior
+default.paginate.next=Próximo
+
+# Mensagens de erro em atribuição de valores. Use "typeMismatch.$className.$propertyName" para personalizar(eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=O campo {0} deve ser um URL válido.
+typeMismatch.java.net.URI=O campo {0} deve ser um URI válido.
+typeMismatch.java.util.Date=O campo {0} deve ser uma data válida
+typeMismatch.java.lang.Double=O campo {0} deve ser um número válido.
+typeMismatch.java.lang.Integer=O campo {0} deve ser um número válido.
+typeMismatch.java.lang.Long=O campo {0} deve ser um número valido.
+typeMismatch.java.lang.Short=O campo {0} deve ser um número válido.
+typeMismatch.java.math.BigDecimal=O campo {0} deve ser um número válido.
+typeMismatch.java.math.BigInteger=O campo {0} deve ser um número válido.
31 test/projects/qkiss/grails-app/i18n/messages_ru.properties
@@ -0,0 +1,31 @@
+default.doesnt.match.message=Значение [{2}] поля [{0}] класса [{1}] не соответствует образцу [{3}]
+default.invalid.url.message=Значение [{2}] поля [{0}] класса [{1}] не является допустимым URL-адресом
+default.invalid.creditCard.message=Значение [{2}] поля [{0}] класса [{1}] не является допустимым номером кредитной карты
+default.invalid.email.message=Значение [{2}] поля [{0}] класса [{1}] не является допустимым e-mail адресом
+default.invalid.range.message=Значение [{2}] поля [{0}] класса [{1}] не попадает в допустимый интервал от [{3}] до [{4}]
+default.invalid.size.message=Размер поля [{0}] класса [{1}] (значение: [{2}]) не попадает в допустимый интервал от [{3}] до [{4}]
+default.invalid.max.message=Значение [{2}] поля [{0}] класса [{1}] больше чем максимально допустимое значение [{3}]
+default.invalid.min.message=Значение [{2}] поля [{0}] класса [{1}] меньше чем минимально допустимое значение [{3}]
+default.invalid.max.size.message=Размер поля [{0}] класса [{1}] (значение: [{2}]) больше чем максимально допустимый размер [{3}]
+default.invalid.min.size.message=Размер поля [{0}] класса [{1}] (значение: [{2}]) меньше чем минимально допустимый размер [{3}]
+default.invalid.validator.message=Значение [{2}] поля [{0}] класса [{1}] не допустимо
+default.not.inlist.message=Значение [{2}] поля [{0}] класса [{1}] не попадает в список допустимых значений [{3}]
+default.blank.message=Поле [{0}] класса [{1}] не может быть пустым
+default.not.equal.message=Значение [{2}] поля [{0}] класса [{1}] не может быть равно [{3}]
+default.null.message=Поле [{0}] класса [{1}] не может иметь значение null
+default.not.unique.message=Значение [{2}] поля [{0}] класса [{1}] должно быть уникальным
+
+default.paginate.prev=Предыдушая страница
+default.paginate.next=Следующая страница
+
+# Ошибки при присвоении данных. Для точной настройки для полей классов используйте
+# формат "typeMismatch.$className.$propertyName" (например, typeMismatch.Book.author)
+typeMismatch.java.net.URL=Значение поля {0} не является допустимым URL
+typeMismatch.java.net.URI=Значение поля {0} не является допустимым URI
+typeMismatch.java.util.Date=Значение поля {0} не является допустимой датой
+typeMismatch.java.lang.Double=Значение поля {0} не является допустимым числом
+typeMismatch.java.lang.Integer=Значение поля {0} не является допустимым числом
+typeMismatch.java.lang.Long=Значение поля {0} не является допустимым числом
+typeMismatch.java.lang.Short=Значение поля {0} не является допустимым числом
+typeMismatch.java.math.BigDecimal=Значение поля {0} не является допустимым числом
+typeMismatch.java.math.BigInteger=Значение поля {0} не является допустимым числом
35 test/projects/qkiss/grails-app/i18n/messages_th.properties
@@ -0,0 +1,35 @@
+default.doesnt.match.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ถูกต้องตามรูปแบบที่กำหนดไว้ใน [{3}]
+default.invalid.url.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ถูกต้องตามรูปแบบ URL
+default.invalid.creditCard.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ถูกต้องตามรูปแบบหมายเลขบัตรเครดิต
+default.invalid.email.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ถูกต้องตามรูปแบบอีเมล์
+default.invalid.range.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ได้มีค่าที่ถูกต้องในช่วงจาก [{3}] ถึง [{4}]
+default.invalid.size.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ได้มีขนาดที่ถูกต้องในช่วงจาก [{3}] ถึง [{4}]
+default.invalid.max.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] มีค่าเกิดกว่าค่ามากสุด [{3}]
+default.invalid.min.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] มีค่าน้อยกว่าค่าต่ำสุด [{3}]
+default.invalid.max.size.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] มีขนาดเกินกว่าขนาดมากสุดของ [{3}]
+default.invalid.min.size.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] มีขนาดต่ำกว่าขนาดต่ำสุดของ [{3}]
+default.invalid.validator.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ผ่านการทวนสอบค่าที่ตั้งขึ้น
+default.not.inlist.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่ได้อยู่ในรายการต่อไปนี้ [{3}]
+default.blank.message=คุณสมบัติ [{0}] ของคลาส [{1}] ไม่สามารถเป็นค่าว่างได้
+default.not.equal.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] ไม่สามารถเท่ากับ [{3}] ได้
+default.null.message=คุณสมบัติ [{0}] ของคลาส [{1}] ไม่สามารถเป็น null ได้
+default.not.unique.message=คุณสมบัติ [{0}] ของคลาส [{1}] ซึ่งมีค่าเป็น [{2}] จะต้องไม่ซ้ำ (unique)
+
+default.paginate.prev=ก่อนหน้า
+default.paginate.next=ถัดไป
+
+default.boolean.true=จริง
+default.boolean.false=เท็จ
+default.date.format=dd-MM-yyyy HH:mm:ss z
+default.number.format=0
+
+# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author)
+typeMismatch.java.net.URL=คุณสมบัติ '{0}' จะต้องเป็นค่า URL ที่ถูกต้อง
+typeMismatch.java.net.URI=คุณสมบัติ '{0}' จะต้องเป็นค่า URI ที่ถูกต้อง
+typeMismatch.java.util.Date=คุณสมบัติ '{0}' จะต้องมีค่าเป็นวันที่
+typeMismatch.java.lang.Double=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท Double
+typeMismatch.java.lang.Integer=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท Integer
+typeMismatch.java.lang.Long=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท Long
+typeMismatch.java.lang.Short=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท Short
+typeMismatch.java.math.BigDecimal=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท BigDecimal
+typeMismatch.java.math.BigInteger=คุณสมบัติ '{0}' จะต้องมีค่าเป็นจำนวนประเภท BigInteger
18 test/projects/qkiss/grails-app/i18n/messages_zh_CN.properties
@@ -0,0 +1,18 @@
+default.blank.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u4E0D\u80FD\u4E3A\u7A7A
+default.doesnt.match.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0E\u5B9A\u4E49\u7684\u6A21\u5F0F [{3}]\u4E0D\u5339\u914D
+default.invalid.creditCard.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u662F\u4E00\u4E2A\u6709\u6548\u7684\u4FE1\u7528\u5361\u53F7
+default.invalid.email.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u662F\u4E00\u4E2A\u5408\u6CD5\u7684\u7535\u5B50\u90AE\u4EF6\u5730\u5740
+default.invalid.max.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u6BD4\u6700\u5927\u503C [{3}]\u8FD8\u5927
+default.invalid.max.size.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u7684\u5927\u5C0F\u6BD4\u6700\u5927\u503C [{3}]\u8FD8\u5927
+default.invalid.min.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u6BD4\u6700\u5C0F\u503C [{3}]\u8FD8\u5C0F
+default.invalid.min.size.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u7684\u5927\u5C0F\u6BD4\u6700\u5C0F\u503C [{3}]\u8FD8\u5C0F
+default.invalid.range.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u5728\u5408\u6CD5\u7684\u8303\u56F4\u5185( [{3}] \uFF5E [{4}] )
+default.invalid.size.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u7684\u5927\u5C0F\u4E0D\u5728\u5408\u6CD5\u7684\u8303\u56F4\u5185( [{3}] \uFF5E [{4}] )
+default.invalid.url.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u662F\u4E00\u4E2A\u5408\u6CD5\u7684URL
+default.invalid.validator.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u672A\u80FD\u901A\u8FC7\u81EA\u5B9A\u4E49\u7684\u9A8C\u8BC1
+default.not.equal.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0E[{3}]\u4E0D\u76F8\u7B49
+default.not.inlist.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u4E0D\u5728\u5217\u8868\u7684\u53D6\u503C\u8303\u56F4\u5185
+default.not.unique.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u7684\u503C[{2}]\u5FC5\u987B\u662F\u552F\u4E00\u7684
+default.null.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u4E0D\u80FD\u4E3Anull
+default.paginate.next=\u4E0B\u9875
+default.paginate.prev=\u4E0A\u9875
27 test/projects/qkiss/grails-app/services/qkiss/HunterService.groovy
@@ -0,0 +1,27 @@
+package qkiss
+import java.util.concurrent.atomic.AtomicInteger
+
+class HunterService{
+
+ static transactional = true
+
+ static boughtTheTicket = false
+ static tookTheRide = false
+ static AtomicInteger rideCount = new AtomicInteger(0)
+ static boughtTheTicketAndTookTheRide = false
+
+ def quartzScheduler
+
+
+
+ static void Buy_The_Ticket() {
+ boughtTheTicket = true
+ }
+
+ def takeTheRide(boolean go) {
+ tookTheRide = go
+ rideCount.getAndIncrement()
+ if(boughtTheTicket && tookTheRide) boughtTheTicketAndTookTheRide = true
+ }
+
+}
54 test/projects/qkiss/grails-app/views/error.gsp
@@ -0,0 +1,54 @@
+<html>
+ <head>
+ <title>Grails Runtime Exception</title>
+ <style type="text/css">
+ .message {
+ border: 1px solid black;
+ padding: 5px;
+ background-color:#E9E9E9;
+ }
+ .stack {
+ border: 1px solid black;
+ padding: 5px;
+ overflow:auto;
+ height: 300px;
+ }
+ .snippet {
+ padding: 5px;
+ background-color:white;
+ border:1px solid black;
+ margin:3px;
+ font-family:courier;
+ }
+ </style>
+ </head>
+
+ <body>
+ <h1>Grails Runtime Exception</h1>
+ <h2>Error Details</h2>
+
+ <div class="message">
+ <strong>Error ${request.'javax.servlet.error.status_code'}:</strong> ${request.'javax.servlet.error.message'.encodeAsHTML()}<br/>
+ <strong>Servlet:</strong> ${request.'javax.servlet.error.servlet_name'}<br/>
+ <strong>URI:</strong> ${request.'javax.servlet.error.request_uri'}<br/>
+ <g:if test="${exception}">
+ <strong>Exception Message:</strong> ${exception.message?.encodeAsHTML()} <br />
+ <strong>Caused by:</strong> ${exception.cause?.message?.encodeAsHTML()} <br />
+ <strong>Class:</strong> ${exception.className} <br />
+ <strong>At Line:</strong> [${exception.lineNumber}] <br />
+ <strong>Code Snippet:</strong><br />
+ <div class="snippet">
+ <g:each var="cs" in="${exception.codeSnippet}">
+ ${cs?.encodeAsHTML()}<br />
+ </g:each>
+ </div>
+ </g:if>
+ </div>
+ <g:if test="${exception}">
+ <h2>Stack Trace</h2>
+ <div class="stack">
+ <pre><g:each in="${exception.stackTraceLines}">${it.encodeAsHTML()}<br/></g:each></pre>
+ </div>
+ </g:if>
+ </body>
+</html>
100 test/projects/qkiss/grails-app/views/index.gsp
@@ -0,0 +1,100 @@
+<html>
+ <head>
+ <title>Welcome to Grails</title>
+ <meta name="layout" content="main" />
+ <style type="text/css" media="screen">
+
+ #nav {
+ margin-top:20px;
+ margin-left:30px;
+ width:228px;
+ float:left;
+
+ }
+ .homePagePanel * {
+ margin:0px;
+ }
+ .homePagePanel .panelBody ul {
+ list-style-type:none;
+ margin-bottom:10px;
+ }
+ .homePagePanel .panelBody h1 {
+ text-transform:uppercase;
+ font-size:1.1em;
+ margin-bottom:10px;
+ }
+ .homePagePanel .panelBody {
+ background: url(images/leftnav_midstretch.png) repeat-y top;
+ margin:0px;
+ padding:15px;
+ }
+ .homePagePanel .panelBtm {
+ background: url(images/leftnav_btm.png) no-repeat top;
+ height:20px;
+ margin:0px;
+ }
+
+ .homePagePanel .panelTop {
+ background: url(images/leftnav_top.png) no-repeat top;
+ height:11px;
+ margin:0px;
+ }
+ h2 {
+ margin-top:15px;
+ margin-bottom:15px;
+ font-size:1.2em;
+ }
+ #pageBody {
+ margin-left:280px;
+ margin-right:20px;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="nav">
+ <div class="homePagePanel">
+ <div class="panelTop"></div>
+ <div class="panelBody">
+ <h1>Application Status</h1>
+ <ul>
+ <li>App version: <g:meta name="app.version"></g:meta></li>
+ <li>Grails version: <g:meta name="app.grails.version"></g:meta></li>
+ <li>Groovy version: ${org.codehaus.groovy.runtime.InvokerHelper.getVersion()}</li>
+ <li>JVM version: ${System.getProperty('java.version')}</li>
+ <li>Controllers: ${grailsApplication.controllerClasses.size()}</li>
+ <li>Domains: ${grailsApplication.domainClasses.size()}</li>
+ <li>Services: ${grailsApplication.serviceClasses.size()}</li>
+ <li>Tag Libraries: ${grailsApplication.tagLibClasses.size()}</li>
+ </ul>
+ <h1>Installed Plugins</h1>
+ <ul>
+ <g:set var="pluginManager"
+ value="${applicationContext.getBean('pluginManager')}"></g:set>
+
+ <g:each var="plugin" in="${pluginManager.allPlugins}">
+ <li>${plugin.name} - ${plugin.version}</li>
+ </g:each>
+
+ </ul>
+ </div>
+ <div class="panelBtm"></div>
+ </div>
+ </div>
+ <div id="pageBody">
+ <h1>Welcome to Grails</h1>
+ <p>Congratulations, you have successfully started your first Grails application! At the moment
+ this is the default page, feel free to modify it to either redirect to a controller or display whatever
+ content you may choose. Below is a list of controllers that are currently deployed in this application,
+ click on each to execute its default action:</p>
+
+ <div id="controllerList" class="dialog">
+ <h2>Available Controllers:</h2>
+ <ul>
+ <g:each var="c" in="${grailsApplication.controllerClasses.sort { it.fullName } }">
+ <li class="controller"><g:link controller="${c.logicalPropertyName}">${c.fullName}</g:link></li>
+ </g:each>
+ </ul>
+ </div>
+ </div>
+ </body>
+</html>
17 test/projects/qkiss/grails-app/views/layouts/main.gsp
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title><g:layoutTitle default="Grails" /></title>