├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── reproducible-config.gradle ├── test-config.gradle ├── java-config.gradle ├── rat-root-config.gradle ├── docs-config.gradle └── publish-config.gradle ├── .sdkmanrc ├── .gitignore ├── NOTICE ├── src ├── main │ ├── templates │ │ └── Job.groovy │ ├── groovy │ │ └── grails │ │ │ └── plugins │ │ │ └── quartz │ │ │ ├── QuartzJobTraitInjector.groovy │ │ │ ├── TriggerDescriptor.groovy │ │ │ ├── listeners │ │ │ ├── ExceptionPrinterJobListener.java │ │ │ └── SessionBinderJobListener.java │ │ │ ├── JobDescriptor.groovy │ │ │ ├── cleanup │ │ │ └── JdbcCleanup.groovy │ │ │ ├── TriggerUtils.groovy │ │ │ ├── GrailsJobClass.java │ │ │ ├── GrailsJobClassConstants.java │ │ │ ├── JobArtefactHandler.groovy │ │ │ ├── QuartzJob.groovy │ │ │ ├── JobDetailFactoryBean.java │ │ │ ├── DefaultGrailsJobClass.java │ │ │ ├── CustomTriggerFactoryBean.java │ │ │ └── GrailsJobFactory.java │ └── scripts │ │ └── CreateJob.groovy ├── docs │ ├── ref │ │ ├── Command Line │ │ │ └── create-job.adoc │ │ └── Triggers │ │ │ ├── simple.adoc │ │ │ ├── cron.adoc │ │ │ └── custom.adoc │ ├── introduction.adoc │ ├── index.adoc │ ├── scheduling.adoc │ ├── triggers.adoc │ └── configuration.adoc └── test │ └── groovy │ └── grails │ └── plugins │ └── quartz │ ├── TestQuartzJob.groovy │ ├── QuartzJobTraitInjectorSpec.groovy │ ├── MockDoWithSpring.groovy │ ├── JobDescriptorSpec.groovy │ ├── JobArtefactHandlerSpec.groovy │ ├── TriggerDescriptorSpec.groovy │ ├── DefaultGrailsJobClassSpec.groovy │ ├── CustomTriggerFactoryBeanSpec.groovy │ ├── JobDetailFactoryBeanSpec.groovy │ └── config │ └── TriggersConfigBuilderSpec.groovy ├── CODE_OF_CONDUCT.md ├── .github ├── vote_templates │ ├── vote_succeeded.txt │ ├── announce.txt │ └── staged.txt ├── renovate.json ├── workflows │ ├── release-notes.yml │ ├── rat.yml │ ├── gradle.yml │ └── release-abort.yml ├── scripts │ ├── releaseDistributions.sh │ └── releaseJarFiles.sh └── release-drafter.yml ├── HEADER ├── grails-app ├── conf │ └── plugin.yml └── services │ └── grails │ └── plugins │ └── quartz │ └── JobManagerService.groovy ├── gradle-bootstrap ├── settings.gradle └── build.gradle ├── .asf.yaml ├── gradle.properties ├── etc └── bin │ ├── reset-verify.sh │ ├── Dockerfile │ ├── extract-build-artifact.sh │ ├── test-reproducible-builds.sh │ ├── verify.sh │ ├── verify-source-distribution.sh │ ├── generate-build-artifact-hashes.groovy │ ├── download-release-artifacts.sh │ ├── verify-jar-artifacts.sh │ └── verify-reproducible.sh ├── settings.gradle ├── gradlew.bat ├── README.md └── gradlew /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-quartz/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.sdkmanrc: -------------------------------------------------------------------------------- 1 | # Enable auto-env through the sdkman_auto_env config - https://sdkman.io/usage#env 2 | java=17.0.15-librca 3 | gradle=8.14.3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | .idea/ 3 | build/ 4 | classes/ 5 | .project 6 | .settings 7 | .classpath 8 | *.iml 9 | *.ipr 10 | *.iws 11 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Apache Grails Quartz Plugin 2 | Copyright 2007-2025 The Apache Software Foundation 3 | 4 | This product includes software developed at 5 | The Apache Software Foundation (http://www.apache.org/). -------------------------------------------------------------------------------- /src/main/templates/Job.groovy: -------------------------------------------------------------------------------- 1 | package ${packageName} 2 | 3 | class ${className}Job { 4 | static triggers = { 5 | simple repeatInterval: 5000l // execute job once in 5 seconds 6 | } 7 | 8 | def execute() { 9 | // execute job 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Apache Grails follows the ASF [Code of Conduct](https://www.apache.org/foundation/policies/conduct). 4 | 5 | If you observe behavior that violates those rules, please follow the 6 | [ASF reporting guidelines](https://www.apache.org/foundation/policies/conduct#reporting-guidelines). 7 | -------------------------------------------------------------------------------- /.github/vote_templates/vote_succeeded.txt: -------------------------------------------------------------------------------- 1 | The vote has passed with +1 binding votes and +1 additional votes. 2 | 3 | 4 | Vote thread: 5 | 6 | 7 | I'll proceed with the release and announce it shortly. 8 | 9 | Thanks to everyone who participated in the vote! 10 | 11 | Regards 12 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "labels": ["type: dependency upgrade"], 6 | "packageRules": [ 7 | { 8 | "matchUpdateTypes": ["major"], 9 | "enabled": false 10 | }, 11 | { 12 | "matchPackagePatterns": ["*"], 13 | "allowedVersions": "!/SNAPSHOT$/" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /.github/vote_templates/announce.txt: -------------------------------------------------------------------------------- 1 | The Apache Grails community is pleased to announce the release of ${PROJECT_NAME} ${VERSION}. 2 | 3 | ${PROJECT_DESC} 4 | 5 | More information can be found here: https://apache.github.io/${REPO_NAME}/${VERSION}/guide 6 | 7 | The release notes are available here: 8 | https://github.com/${REPO_SLUG}/releases/tag/${TAG} 9 | 10 | Apache Grails website: https://grails.apache.org/ 11 | 12 | Download Links: https://grails.apache.org/download.html 13 | 14 | Grails Resources: 15 | - ${PROJECT_NAME} GitHub repo: https://github.com/${REPO_SLUG} 16 | - Issues: https://github.com/${REPO_SLUG}/issues 17 | - Mailing lists: https://grails.apache.org/community.html 18 | 19 | Happy Coding, 20 | The Apache Grails Team 21 | -------------------------------------------------------------------------------- /HEADER: -------------------------------------------------------------------------------- 1 | Licensed to the Apache Software Foundation (ASF) under one 2 | or more contributor license agreements. See the NOTICE file 3 | distributed with this work for additional information 4 | regarding copyright ownership. The ASF licenses this file 5 | to you under the Apache License, Version 2.0 (the 6 | "License"); you may not use this file except in compliance 7 | with the License. You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, 12 | software distributed under the License is distributed on an 13 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | KIND, either express or implied. See the License for the 15 | specific language governing permissions and limitations 16 | under the License. -------------------------------------------------------------------------------- /grails-app/conf/plugin.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | quartz: 17 | jdbcStore: false 18 | -------------------------------------------------------------------------------- /gradle-bootstrap/settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | -------------------------------------------------------------------------------- /.asf.yaml: -------------------------------------------------------------------------------- 1 | github: 2 | environments: 3 | release: 4 | required_reviewers: 5 | - id: grails-committers 6 | type: Team 7 | - id: jdaugherty 8 | type: User 9 | - id: matrei 10 | type: User 11 | - id: jamesfredley 12 | type: User 13 | - id: sbglasius 14 | type: User 15 | wait_timer: 0 16 | docs: 17 | required_reviewers: 18 | - id: grails-committers 19 | type: Team 20 | - id: jdaugherty 21 | type: User 22 | - id: matrei 23 | type: User 24 | - id: jamesfredley 25 | type: User 26 | - id: sbglasius 27 | type: User 28 | wait_timer: 0 29 | close: 30 | required_reviewers: 31 | - id: grails-committers 32 | type: Team 33 | - id: jdaugherty 34 | type: User 35 | - id: matrei 36 | type: User 37 | - id: jamesfredley 38 | type: User 39 | - id: sbglasius 40 | type: User 41 | wait_timer: 0 42 | -------------------------------------------------------------------------------- /src/docs/ref/Command Line/create-job.adoc: -------------------------------------------------------------------------------- 1 | //// 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | //// 19 | 20 | [[ref-command-line-create-job]] 21 | ==== create-job 22 | 23 | ===== Purpose 24 | 25 | Create a job class 26 | 27 | ===== Examples 28 | 29 | [,console] 30 | ---- 31 | grails create-job MyJob 32 | ---- 33 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugins/quartz/QuartzJobTraitInjector.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package grails.plugins.quartz 17 | 18 | import grails.compiler.traits.TraitInjector 19 | import groovy.transform.CompileStatic 20 | 21 | @CompileStatic 22 | class QuartzJobTraitInjector implements TraitInjector { 23 | 24 | @Override 25 | Class getTrait() { 26 | QuartzJob 27 | } 28 | 29 | @Override 30 | String[] getArtefactTypes() { 31 | [DefaultGrailsJobClass.JOB] as String[] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugins/quartz/TestQuartzJob.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package grails.plugins.quartz 21 | 22 | import org.quartz.Job 23 | import org.quartz.JobExecutionContext 24 | import org.quartz.JobExecutionException 25 | 26 | /** 27 | * @author Vitalii Samolovskikh aka Kefir 28 | */ 29 | class TestQuartzJob implements Job { 30 | @Override 31 | void execute(JobExecutionContext context) throws JobExecutionException {} 32 | } 33 | -------------------------------------------------------------------------------- /src/docs/introduction.adoc: -------------------------------------------------------------------------------- 1 | //// 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | //// 19 | 20 | [[introduction]] 21 | == Introduction 22 | 23 | The Quartz Plugin allows your Grails application to schedule jobs to be executed using a specified interval or cron expression. The underlying system uses the https://www.quartz-scheduler.org/[Quartz Enterprise Job Scheduler] configured via Spring, but is made simpler by the coding-by-convention paradigm. 24 | Since version `1.0-RC3`, this plugin requires Quartz version `2.1.x` or higher and no longer supports Quartz version `1.8.x`. 25 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugins/quartz/QuartzJobTraitInjectorSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package grails.plugins.quartz 21 | 22 | import grails.artefact.Artefact 23 | import spock.lang.Specification 24 | 25 | class QuartzJobTraitInjectorSpec extends Specification { 26 | 27 | void 'test that the job trait is applied'() { 28 | expect: 29 | QuartzJob.isAssignableFrom TraitTestJob 30 | } 31 | } 32 | 33 | @Artefact('Job') 34 | class TraitTestJob {} 35 | -------------------------------------------------------------------------------- /src/docs/ref/Triggers/simple.adoc: -------------------------------------------------------------------------------- 1 | //// 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | //// 19 | 20 | 21 | [[ref-triggers-simple]] 22 | ==== simple 23 | 24 | 25 | ===== Purpose 26 | 27 | A simple trigger that executes on a set interval. 28 | 29 | 30 | ===== Examples 31 | 32 | [source,groovy] 33 | ---- 34 | class MyJob { 35 | 36 | static triggers = { 37 | simple( 38 | name: 'simpleTrigger', 39 | startDelay: 10000, 40 | repeatInterval: 30000, 41 | repeatCount: 10 42 | ) 43 | } 44 | } 45 | ---- 46 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | projectVersion=4.0.1-SNAPSHOT 20 | grailsVersion=7.0.0 21 | javaVersion=17 22 | 23 | asciidoctorGradlePluginVersion=4.0.4 24 | gradleCryptoChecksumVersion=1.4.0 25 | ratVersion=0.8.1 26 | 27 | # This prevents the Grails Gradle Plugin from unnecessarily excluding slf4j-simple in the generated POMs 28 | # https://github.com/apache/grails-gradle-plugin/issues/222 29 | slf4jPreventExclusion=true 30 | 31 | org.gradle.caching=true 32 | org.gradle.daemon=true 33 | org.gradle.parallel=true 34 | -------------------------------------------------------------------------------- /src/docs/index.adoc: -------------------------------------------------------------------------------- 1 | //// 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | //// 19 | 20 | = Grails Quartz Plugin - Reference Documentation 21 | 22 | include::introduction.adoc[] 23 | 24 | include::scheduling.adoc[] 25 | 26 | include::triggers.adoc[] 27 | 28 | include::configuration.adoc[] 29 | 30 | [[reference]] 31 | == Reference 32 | 33 | [[ref-command-line]] 34 | === Command Line 35 | 36 | include::ref/Command Line/create-job.adoc[] 37 | 38 | [[ref-triggers]] 39 | === Triggers 40 | 41 | include::ref/Triggers/cron.adoc[] 42 | 43 | include::ref/Triggers/custom.adoc[] 44 | 45 | include::ref/Triggers/simple.adoc[] 46 | 47 | -------------------------------------------------------------------------------- /.github/vote_templates/staged.txt: -------------------------------------------------------------------------------- 1 | Hi Everyone, 2 | 3 | I am happy to start the VOTE thread for an ${PROJECT_NAME} release of version ${VERSION}! 4 | 5 | Release notes for the release are here: 6 | https://github.com/${REPO_SLUG}/releases/tag/${TAG} 7 | 8 | The tag for this release is: 9 | https://github.com/${REPO_SLUG}/releases/tag/${TAG} 10 | Tag commit id: ${SHA} 11 | 12 | The artifacts to be voted on are located as follows (r${DIST_SVN_REVISION}): 13 | Source release: https://dist.apache.org/repos/dist/dev/grails/${SVN_FOLDER}/${VERSION}/sources 14 | 15 | Release artifacts are signed with a key from the following file: 16 | https://dist.apache.org/repos/dist/release/grails/KEYS 17 | 18 | Please vote on releasing this package as: ${PROJECT_NAME} ${VERSION}. 19 | 20 | Reminder on ASF release approval requirements for PMC members: 21 | https://www.apache.org/legal/release-policy.html#release-approval 22 | 23 | Hints on validating checksums/signatures (but replace md5sum with sha512sum): 24 | https://www.apache.org/info/verification.html 25 | 26 | The vote is open for a minimum of 72 hours and passes if a majority of at least 27 | three +1 PMC votes are cast. 28 | 29 | [ ] +1 Release ${PROJECT_NAME} ${VERSION} 30 | [ ] 0 I don't have a strong opinion about this, but I assume it's ok 31 | [ ] -1 Do not release ${PROJECT_NAME} ${VERSION} because... 32 | 33 | Here is my vote: 34 | 35 | +1 (binding) 36 | -------------------------------------------------------------------------------- /src/docs/ref/Triggers/cron.adoc: -------------------------------------------------------------------------------- 1 | //// 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | //// 19 | 20 | [[ref-triggers-cron]] 21 | ==== cron 22 | 23 | 24 | ===== Purpose 25 | 26 | A cron trigger that executes based on the defined cron expression 27 | 28 | 29 | ===== Examples 30 | 31 | [source,groovy] 32 | ---- 33 | class MyJob { 34 | 35 | static triggers = { 36 | cron( 37 | name: 'cronTrigger', 38 | startDelay: 10000, 39 | cronExpression: '0/6 * 15 * * ?', 40 | timeZone: TimeZone.getTimeZone('GMT-8') // timeZone is optional 41 | ) 42 | } 43 | 44 | void execute() { 45 | println 'Job run!' 46 | } 47 | } 48 | ---- 49 | -------------------------------------------------------------------------------- /src/docs/ref/Triggers/custom.adoc: -------------------------------------------------------------------------------- 1 | //// 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | //// 19 | 20 | 21 | [[ref-triggers-custom]] 22 | ==== custom 23 | 24 | 25 | ===== Purpose 26 | 27 | A custom trigger that executes based on the result of a custom `Trigger` implementation 28 | 29 | 30 | ===== Examples 31 | 32 | [source,groovy] 33 | ---- 34 | class MyJob { 35 | 36 | static triggers = { 37 | custom( 38 | name: 'customTrigger', 39 | triggerClass: MyTriggerClass, 40 | myParam: myValue, 41 | myAnotherParam: myAnotherValue 42 | ) 43 | } 44 | 45 | void execute() { 46 | println 'Job run!' 47 | } 48 | } 49 | ---- 50 | -------------------------------------------------------------------------------- /gradle/reproducible-config.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | // Any jar, zip, or archive should be reproducible 21 | // No longer needed after https://github.com/gradle/gradle/issues/30871 22 | tasks.withType(AbstractArchiveTask).configureEach { 23 | preserveFileTimestamps = false // to prevent timestamp mismatches 24 | reproducibleFileOrder = true // to keep the same ordering 25 | // to avoid platform specific defaults, set the permissions consistently 26 | filePermissions { permissions -> 27 | permissions.unix(0644) 28 | } 29 | dirPermissions { permissions -> 30 | permissions.unix(0755) 31 | } 32 | } -------------------------------------------------------------------------------- /.github/workflows/release-notes.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | name: "Release Drafter" 17 | on: 18 | issues: 19 | types: [closed,reopened] 20 | push: 21 | branches: 22 | - '[3-9]+.[0-9]+.x' 23 | pull_request: 24 | types: [opened, reopened, synchronize] 25 | pull_request_target: 26 | types: [opened, reopened, synchronize] 27 | workflow_dispatch: 28 | jobs: 29 | update_release_draft: 30 | permissions: 31 | contents: read # limit to read access 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: "📝 Update Release Draft" 35 | uses: release-drafter/release-drafter@v6 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GRAILS_GH_TOKEN }} 38 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugins/quartz/MockDoWithSpring.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grails.plugins.quartz 18 | 19 | class MockDoWithSpring { 20 | 21 | def quartzProperties 22 | def application = [jobClasses: null, config: new ConfigObject()] 23 | def manager 24 | 25 | def ref(whatever) { 26 | null 27 | } 28 | 29 | def quartzJobFactory(whatever) { 30 | null 31 | } 32 | 33 | def exceptionPrinterListener(whatever) { 34 | null 35 | } 36 | 37 | def sessionBinderListener(something, whatever) { 38 | null 39 | } 40 | 41 | void quartzScheduler(whatever, Closure props) { 42 | def data = [:] 43 | props.delegate = data 44 | props.resolveStrategy = Closure.DELEGATE_FIRST 45 | props.call([:]) 46 | println "xxxxxxxx=$data" 47 | this.quartzProperties = data.quartzProperties 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /etc/bin/reset-verify.sh: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | PROJECT_NAME='grails-quartz' 20 | RELEASE_TAG=$1 21 | DOWNLOAD_LOCATION="${2:-downloads}" 22 | DOWNLOAD_LOCATION=$(realpath "${DOWNLOAD_LOCATION}") 23 | 24 | if [ -z "${RELEASE_TAG}" ]; then 25 | echo "Usage: $0 [release-tag] " 26 | exit 1 27 | fi 28 | 29 | VERSION=${RELEASE_TAG#v} 30 | 31 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 32 | CWD=$(pwd) 33 | 34 | cd "${DOWNLOAD_LOCATION}" 35 | rm *.zip *.asc *.sha512 36 | cd "${PROJECT_NAME}" 37 | find . -mindepth 1 -path ./etc -prune -o -exec rm -rf {} + 38 | cd etc 39 | find . -mindepth 1 -path ./bin -prune -o -exec rm -rf {} + 40 | cd bin 41 | find . -mindepth 1 -path ./results -prune -o -exec rm -rf {} + 42 | cd "${CWD}" -------------------------------------------------------------------------------- /src/main/scripts/CreateJob.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | description("Creates a new Quartz scheduled job") { 17 | usage "grails create-job [JOB NAME]" 18 | argument name:'Job Name', description:"The name of the job" 19 | } 20 | 21 | model = model(trimTrailingJobFromJobName(args[0]) ) 22 | render template:"Job.groovy", 23 | destination: file( "grails-app/jobs/$model.packagePath/${trimTrailingJobFromJobName(model.simpleName)}Job.groovy"), 24 | model: model 25 | 26 | /** 27 | * //if 'Job' already exists in the end of JobName, then remove it from jobName. 28 | * @param name 29 | * @return 30 | */ 31 | String trimTrailingJobFromJobName(String name){ 32 | String type = "Job" 33 | String processedName = name 34 | Integer lastIndexOfJOBInJobName = name.lastIndexOf(type) 35 | if(lastIndexOfJOBInJobName == (name.length() - type.length())){ 36 | processedName = name.substring(0, lastIndexOfJOBInJobName) 37 | } 38 | return processedName 39 | } -------------------------------------------------------------------------------- /gradle/test-config.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | dependencies { 21 | add('testRuntimeOnly', 'org.slf4j:slf4j-nop') // Get rid of warning about missing slf4j implementation during test task 22 | add('testRuntimeOnly', 'org.junit.platform:junit-platform-launcher') // Gradle 9+ requires this 23 | } 24 | 25 | tasks.withType(Test).configureEach { 26 | onlyIf { !project.hasProperty('skipTests') } 27 | useJUnitPlatform() 28 | testLogging { 29 | exceptionFormat = 'full' 30 | events('passed', 'skipped', 'failed') 31 | } 32 | } 33 | 34 | ['bootRun', 'bootTestRun'].each { taskName -> 35 | tasks.named(taskName) { 36 | // Disable bootRun and bootTestRun tasks as no application class is available 37 | enabled = false 38 | } 39 | } -------------------------------------------------------------------------------- /gradle-bootstrap/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | def props = new Properties() 21 | layout.projectDirectory.file('../.sdkmanrc').asFile.withInputStream { 22 | props.load(it) 23 | } 24 | 25 | tasks.withType(Wrapper).configureEach { 26 | gradleVersion = props.gradle 27 | } 28 | 29 | defaultTasks 'bootstrap' 30 | tasks.register('bootstrap') { 31 | dependsOn('wrapper') 32 | doLast { 33 | ant.move(file: "$projectDir/gradlew", todir: "$projectDir/../") 34 | ant.move(file: "$projectDir/gradlew.bat", todir: "$projectDir/../") 35 | ant.move(file: "$projectDir/gradle/wrapper", todir: "$projectDir/../gradle") 36 | ant.chmod(file: "$projectDir/../gradlew", perm: '755') 37 | ant.chmod(file: "$projectDir/../gradlew.bat", perm: '755') 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugins/quartz/TriggerDescriptor.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grails.plugins.quartz 18 | 19 | import groovy.transform.CompileStatic 20 | import org.quartz.Scheduler 21 | import org.quartz.Trigger 22 | 23 | /** 24 | * TriggerDescriptor that stores information about the Quartz trigger to show on webapp. 25 | * 26 | * @author Sergey Nebolsin (nebolsin@gmail.com) 27 | * 28 | * @since 1.0 29 | */ 30 | @CompileStatic 31 | class TriggerDescriptor { 32 | JobDescriptor jobDescriptor 33 | 34 | Trigger trigger 35 | 36 | Trigger.TriggerState state 37 | 38 | static build(JobDescriptor jobDescriptor, Trigger trigger, Scheduler scheduler) { 39 | def result = new TriggerDescriptor(jobDescriptor: jobDescriptor, trigger: trigger) 40 | result.state = scheduler.getTriggerState(trigger.key) 41 | return result 42 | } 43 | 44 | String getName() { 45 | trigger.key.name 46 | } 47 | 48 | String getGroup() { 49 | trigger.key.group 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /gradle/java-config.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | compileJava.options.release = javaVersion.toInteger() 21 | 22 | tasks.withType(Javadoc).configureEach { 23 | (options as StandardJavadocDocletOptions).noTimestamp(true) // prevent the file header with the date 24 | (options as StandardJavadocDocletOptions).bottom("Generated $formattedBuildDate (UTC)") 25 | } 26 | 27 | tasks.withType(GroovyCompile).configureEach { 28 | groovyOptions.encoding = 'UTF-8' // encoding needs to be the same since it's different across platforms 29 | // Preserve method parameter names in Groovy classes for IDE parameter hints. 30 | groovyOptions.parameters = true 31 | options.encoding = 'UTF-8' // encoding needs to be the same since it's different across platforms 32 | options.fork = true 33 | options.forkOptions.jvmArgs = ['-Xms128M', '-Xmx2G'] 34 | } -------------------------------------------------------------------------------- /src/main/groovy/grails/plugins/quartz/listeners/ExceptionPrinterJobListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grails.plugins.quartz.listeners; 18 | 19 | import org.quartz.JobExecutionContext; 20 | import org.quartz.JobExecutionException; 21 | import org.quartz.listeners.JobListenerSupport; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | /** 26 | * JobListener implementation which logs an exceptions occurred during job's execution. 27 | * 28 | * @author Sergey Nebolsin (nebolsin@gmail.com) 29 | * @since 0.2 30 | */ 31 | public class ExceptionPrinterJobListener extends JobListenerSupport { 32 | 33 | private static final Logger LOG = LoggerFactory.getLogger(ExceptionPrinterJobListener.class); 34 | 35 | public static final String NAME = "exceptionPrinterListener"; 36 | 37 | public String getName() { 38 | return NAME; 39 | } 40 | 41 | public void jobWasExecuted(JobExecutionContext context, JobExecutionException exception) { 42 | if (exception != null) { 43 | LOG.error("Exception occurred in job: " + context.getJobDetail().getDescription(), exception); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/groovy/grails/plugins/quartz/JobDescriptor.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grails.plugins.quartz 18 | 19 | import groovy.transform.CompileStatic 20 | import org.quartz.JobDetail 21 | import org.quartz.Scheduler 22 | import org.quartz.Trigger 23 | 24 | /** 25 | * JobDescriptor that stores information about the Quartz job to show on webapp. 26 | * 27 | * @author Marco Mornati (mmornati@byte-code.com) 28 | * @author Sergey Nebolsin (nebolsin@gmail.com) 29 | * 30 | * @since 0.4 31 | */ 32 | @CompileStatic 33 | class JobDescriptor { 34 | JobDetail jobDetail 35 | 36 | List triggerDescriptors 37 | 38 | static build(JobDetail jobDetail, Scheduler scheduler) { 39 | def job = new JobDescriptor(jobDetail: jobDetail) 40 | job.triggerDescriptors = (List )scheduler.getTriggersOfJob(jobDetail.key).collect { trigger -> 41 | TriggerDescriptor.build(job, (Trigger)trigger, scheduler) 42 | } 43 | return job 44 | } 45 | 46 | String getName() { 47 | jobDetail.key.name 48 | } 49 | 50 | String getGroup() { 51 | jobDetail.key.group 52 | } 53 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | plugins { 21 | id 'com.gradle.develocity' version '4.0.2' 22 | id 'com.gradle.common-custom-user-data-gradle-plugin' version '2.3' 23 | } 24 | 25 | def isCI = System.getenv().containsKey('CI') 26 | def isLocal = !isCI 27 | def isReproducibleBuild = System.getenv('SOURCE_DATE_EPOCH') != null 28 | if (isReproducibleBuild) { 29 | gradle.settingsEvaluated { 30 | logger.warn('*************** Remote Build Cache Disabled due to Reproducible Build ********************') 31 | logger.warn('Build date will be set to (SOURCE_DATE_EPOCH={})', System.getenv('SOURCE_DATE_EPOCH')) 32 | } 33 | } 34 | 35 | develocity { 36 | server = 'https://ge.grails.org' 37 | buildScan { 38 | tag('grails') 39 | tag('grails-quartz') 40 | publishing.onlyIf { it.authenticated } 41 | uploadInBackground = isLocal 42 | } 43 | } 44 | 45 | buildCache { 46 | local { enabled = (isLocal && !isReproducibleBuild) || (isCI && isReproducibleBuild) } 47 | remote(develocity.buildCache) { 48 | push = isCI 49 | enabled = !isReproducibleBuild 50 | } 51 | } 52 | 53 | rootProject.name = 'grails-quartz' 54 | -------------------------------------------------------------------------------- /etc/bin/Dockerfile: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # for testing in a container that is similar to the github action linux build environment 17 | # run this from the root of the project 18 | # `docker build -t grails:testing -f etc/bin/Dockerfile . && docker run -it --rm -v $(pwd):/home/groovy/project grails:testing bash` 19 | FROM bellsoft/liberica-openjdk-debian:17.0.15 20 | 21 | USER root 22 | RUN apt-get update && apt-get install -y curl unzip coreutils libdigest-sha-perl gpg vim sudo psmisc locales groovy rsync 23 | 24 | RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ 25 | dpkg-reconfigure --frontend=noninteractive locales && \ 26 | update-locale LANG=en_US.UTF-8 27 | 28 | RUN useradd --system --create-home --home-dir /home/groovy groovy 29 | RUN usermod -s /bin/bash -g root -G sudo groovy 30 | RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers 31 | USER groovy 32 | 33 | WORKDIR /home/groovy 34 | RUN mkdir -p /home/groovy/scripts/etc/bin && mkdir -p /home/groovy/scripts/gradle/wrapper && mkdir -p /home/groovy/grails-verify && mkdir -p /home/groovy/project 35 | ADD --chown=groovy etc/bin /home/groovy/scripts/etc/bin 36 | ADD --chown=groovy gradlew /home/groovy/scripts 37 | ADD --chown=groovy gradle/wrapper/gradle-wrapper.jar /home/groovy/scripts/gradle/wrapper 38 | ADD --chown=groovy gradle/wrapper/gradle-wrapper.properties /home/groovy/scripts/gradle/wrapper 39 | ENV PATH="/home/groovy/scripts:/home/groovy/scripts/etc/bin:$PATH" 40 | ENV CI=true 41 | ENV LANG=C.UTF-8 42 | ENV LC_ALL=en_US.UTF-8 43 | ENV LC_CTYPE=en_US.UTF-8 44 | 45 | CMD ["/bin/bash", "-ec", "while :; do echo '.'; sleep 1000 ; done"] -------------------------------------------------------------------------------- /src/test/groovy/grails/plugins/quartz/JobDescriptorSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package grails.plugins.quartz 21 | 22 | import org.quartz.* 23 | import org.quartz.impl.StdSchedulerFactory 24 | import spock.lang.Specification 25 | 26 | /** 27 | * Unit tests for JobDescriptor. 28 | * 29 | * @author Vitalii Samolovskikh aka Kefir 30 | */ 31 | class JobDescriptorSpec extends Specification { 32 | 33 | private Scheduler scheduler 34 | private JobDetail job 35 | private Trigger trigger 36 | 37 | 38 | def setup() { 39 | scheduler = StdSchedulerFactory.getDefaultScheduler() 40 | scheduler.start() 41 | job = JobBuilder.newJob(TestQuartzJob).withIdentity(new JobKey("job", "group")).build() 42 | trigger = TriggerBuilder.newTrigger() 43 | .withIdentity(new TriggerKey("trigger", "group")) 44 | .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInMinutes(2).repeatForever()) 45 | .startNow() 46 | .build() 47 | scheduler.scheduleJob(job, trigger) 48 | } 49 | 50 | def cleanup() { 51 | scheduler.shutdown() 52 | } 53 | 54 | void 'JobDescriptor builds correctly from a job and scheduler'() { 55 | when: 56 | JobDescriptor descriptor = JobDescriptor.build(job, scheduler) 57 | then: 58 | descriptor.name == 'job' 59 | descriptor.group == 'group' 60 | descriptor.triggerDescriptors.size() == 1 61 | descriptor.triggerDescriptors[0].name == 'trigger' 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /etc/bin/extract-build-artifact.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # 20 | set -e 21 | 22 | ARTIFACT_NAME=$1 23 | 24 | if [ -z "${ARTIFACT_NAME}" ]; then 25 | echo "Usage: $0 " 26 | exit 1 27 | fi 28 | 29 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 30 | EXTRACT_LOCATION="${2:-${SCRIPT_DIR}/results}" 31 | 32 | echo "Looking for build artifact ${ARTIFACT_NAME} in ${EXTRACT_LOCATION}" 33 | 34 | if [ -z "${EXTRACT_LOCATION}/first/${ARTIFACT_NAME}" ]; then 35 | echo "❌ First Artifact Not found: ${ARTIFACT_NAME} could not be found under ${EXTRACT_LOCATION}/first/${ARTIFACT_NAME}" 36 | exit 1; 37 | else 38 | echo " ✅ First Artifact Found @ ${EXTRACT_LOCATION}/first/${ARTIFACT_NAME}" 39 | fi 40 | if [ -z "${EXTRACT_LOCATION}/second/${ARTIFACT_NAME}" ]; then 41 | echo "❌ Second Artifact Not found: ${ARTIFACT_NAME} could not be found under ${EXTRACT_LOCATION}/second/${ARTIFACT_NAME}" 42 | exit 1; 43 | else 44 | echo " ✅ Second Artifact Found @ ${EXTRACT_LOCATION}/second/${ARTIFACT_NAME}" 45 | fi 46 | 47 | rm -rf "${EXTRACT_LOCATION}/firstArtifact" || true 48 | rm -rf "${EXTRACT_LOCATION}/secondArtifact" || true 49 | 50 | echo " Extracting ${ARTIFACT_NAME} from first to ${EXTRACT_LOCATION}/firstArtifact" 51 | unzip -q "${EXTRACT_LOCATION}/first/${ARTIFACT_NAME}" -d "${EXTRACT_LOCATION}/firstArtifact" 52 | echo " ✅ First Artifact Extracted" 53 | 54 | echo " Extracting ${ARTIFACT_NAME} from second to ${EXTRACT_LOCATION}/secondArtifact" 55 | unzip -q "${EXTRACT_LOCATION}/second/${ARTIFACT_NAME}" -d "${EXTRACT_LOCATION}/secondArtifact" 56 | echo " ✅ Second Artifact Extracted" 57 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugins/quartz/cleanup/JdbcCleanup.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package grails.plugins.quartz.cleanup 21 | 22 | import groovy.sql.Sql 23 | import groovy.util.logging.Slf4j 24 | import jakarta.annotation.PostConstruct; 25 | 26 | 27 | 28 | /** 29 | * Contributed by Rocketmiles 30 | * This class can purge all of the quartz tables on startup if you set the config flag quartz.purgeQuartzTablesOnStartup = true 31 | * Mostly used for testing or development purposes. It is not recommended you use this for production as you can 32 | * miss missfire recoveries, persisted jobs/triggers, etc. 33 | */ 34 | 35 | @Slf4j 36 | public class JdbcCleanup { 37 | 38 | def dataSource 39 | 40 | @PostConstruct 41 | void init() { 42 | 43 | log.info "[quartz-plugin] Purging Quartz tables...." 44 | 45 | def queries = [] 46 | queries.add("DELETE FROM QRTZ_FIRED_TRIGGERS") 47 | queries.add("DELETE FROM QRTZ_PAUSED_TRIGGER_GRPS") 48 | queries.add("DELETE FROM QRTZ_SCHEDULER_STATE") 49 | queries.add("DELETE FROM QRTZ_LOCKS") 50 | queries.add("DELETE FROM QRTZ_SIMPLE_TRIGGERS") 51 | queries.add("DELETE FROM QRTZ_SIMPROP_TRIGGERS") 52 | queries.add("DELETE FROM QRTZ_CRON_TRIGGERS") 53 | queries.add("DELETE FROM QRTZ_BLOB_TRIGGERS") 54 | queries.add("DELETE FROM QRTZ_TRIGGERS") 55 | queries.add("DELETE FROM QRTZ_JOB_DETAILS") 56 | queries.add("DELETE FROM QRTZ_CALENDARS") 57 | 58 | def sql = new Sql(dataSource) 59 | queries.each { query -> 60 | log.info("Executing " + query) 61 | sql.execute(query) 62 | } 63 | 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/rat.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | name: RAT Report 17 | on: 18 | push: 19 | branches: 20 | - '[3-9]+.[0-9]+.x' 21 | - license-audit 22 | pull_request: 23 | branches: 24 | - '[3-9]+.[0-9]+.x' 25 | - license-audit 26 | workflow_dispatch: 27 | # queue jobs and only allow 1 run per branch due to the likelihood of hitting GitHub resource limits 28 | concurrency: 29 | group: ${{ github.workflow }}-${{ github.ref }} 30 | cancel-in-progress: false 31 | jobs: 32 | rat-audit: 33 | runs-on: ubuntu-24.04 34 | steps: 35 | - name: "📥 Checkout repository" 36 | uses: actions/checkout@v4 37 | - name: "☕️ Setup JDK" 38 | uses: actions/setup-java@v4 39 | with: 40 | distribution: liberica 41 | java-version: 17 42 | - name: "🐘 Setup Gradle" 43 | uses: gradle/actions/setup-gradle@v4 44 | with: 45 | develocity-access-key: ${{ secrets.GRAILS_DEVELOCITY_ACCESS_KEY }} 46 | - name: "🧐 Apache License - Release Audit Tool" 47 | run: ./gradlew rat 48 | - name: "📤 Upload RAT HTML report" 49 | if: always() 50 | uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 51 | with: 52 | name: rat-report 53 | path: build/reports/rat/index.html 54 | - name: "🗞️ Publish RAT report in Job Summary" 55 | if: always() 56 | run: | 57 | echo "## 📋 Apache RAT Report" >> $GITHUB_STEP_SUMMARY 58 | # inject raw HTML (it will render as HTML in the summary) 59 | sed -n '/]*>/,/<\/body>/ { /

Archives:<\/h3>/,/<\/body>/d; /]*>/d; /<\/body>/d; s/^[[:space:]]*//; p; }' build/reports/rat/index.html >> $GITHUB_STEP_SUMMARY -------------------------------------------------------------------------------- /src/test/groovy/grails/plugins/quartz/JobArtefactHandlerSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grails.plugins.quartz 18 | 19 | import grails.core.ArtefactHandler 20 | import spock.lang.Specification 21 | 22 | /** 23 | * Test case for Job artefact handler. 24 | * 25 | * @author Sergey Nebolsin 26 | * @since 0.2 27 | */ 28 | class JobArtefactHandlerSpec extends Specification { 29 | 30 | private ArtefactHandler handler = new JobArtefactHandler() 31 | protected GroovyClassLoader gcl = new GroovyClassLoader() 32 | 33 | void 'class with execute() method should be recognized as a Job class'() { 34 | setup: 35 | Class c = gcl.parseClass("class TestJob { def execute() { }}\n") 36 | expect: 37 | assert handler.isArtefact(c): "Class *Job which defines execute() method should be recognized as a Job class" 38 | } 39 | 40 | void 'class with execute(param) method should be recognized as a Job class'() { 41 | setup: 42 | Class c = gcl.parseClass("class TestJob { def execute(param) { }}\n") 43 | expect: 44 | assert handler.isArtefact(c): "Class *Job which defines execute(param) method should be recognized as a Job class" 45 | } 46 | 47 | void 'class with wrong name should not be recognized as a Job class'() { 48 | setup: 49 | Class c = gcl.parseClass("class TestController { def execute() { }}\n") 50 | expect: 51 | assert !handler.isArtefact(c): "Class which name doesn't end with 'Job' shouldn't be recognized as a Job class" 52 | } 53 | 54 | void 'class without execute() method should not be recognized as a Job class'() { 55 | setup: 56 | Class c = gcl.parseClass("class TestJob { def execute1() { }}\n") 57 | expect: 58 | assert !handler.isArtefact(c): "Class which doesn't declare 'execute' method shouldn't be recognized as a Job class" 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /gradle/rat-root-config.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import org.nosphere.apache.rat.RatTask 19 | 20 | apply plugin: 'org.nosphere.apache.rat' 21 | 22 | tasks.named('rat', RatTask) { 23 | excludes.addAll( 24 | '.asf.yaml', // ASF metadata for github integration excluded from src zip 25 | 'CODE_OF_CONDUCT.md', 26 | 'BUILD_DATE', // build artifact for storing the build date / verifying 27 | 'CHECKSUMS', // build artifact for storing checksums for easy verification 28 | 'PUBLISHED_ARTIFACTS', // build artifact for storing published artifacts coordinates & paths 29 | 'licenses/**', // licenses directory excluded 30 | '**/build/**', // Gradle generated build directories 31 | '**/.gitattributes', // git configuration isn't code 32 | '.github/**', // github configuration isn't shipped in the source distro 33 | '**/.gradle/**', '**/wrapper/**', 'gradlew*', // gradle wrapper files excluded from src zip 34 | 'out/**', '*.ipr', '**/*.iml', '*.iws', '.idea/**', // Intellij generated files 35 | '**/.sdkmanrc', // tool selection files aren't code 36 | '**/.gitignore', // git configuration isn't code 37 | '**/.gitkeep', // git configuration isn't code 38 | 'etc/bin/results/**', // exclude build directories 39 | '**/*.png', '**/*.svg', '**/*.ico', '**/*.eps', '**/*.icns', '**/*.jpg', '**/*.jpeg', '**/*.gif', // Image files 40 | '**/*.db', // H2 database test files 41 | '**/*.gitkeep', // Empty Gitkeep file 42 | 'src/main/templates/**', // Exclude template files 43 | ) 44 | // never cache license audits 45 | outputs.upToDateWhen { false } 46 | } -------------------------------------------------------------------------------- /src/test/groovy/grails/plugins/quartz/TriggerDescriptorSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package grails.plugins.quartz 21 | 22 | import org.quartz.JobBuilder 23 | import org.quartz.JobDetail 24 | import org.quartz.JobKey 25 | import org.quartz.Scheduler 26 | import org.quartz.SimpleScheduleBuilder 27 | import org.quartz.Trigger 28 | import org.quartz.TriggerBuilder 29 | import org.quartz.TriggerKey 30 | import org.quartz.impl.StdSchedulerFactory 31 | import spock.lang.Specification 32 | 33 | /** 34 | * Unit tests for TriggerDescriptor 35 | * 36 | * @author Vitalii Samolovskikh aka Kefir 37 | */ 38 | class TriggerDescriptorSpec extends Specification { 39 | 40 | private Scheduler scheduler 41 | private JobDetail job 42 | private Trigger trigger 43 | 44 | def setup() { 45 | scheduler = StdSchedulerFactory.getDefaultScheduler() 46 | 47 | scheduler.start() 48 | 49 | job = JobBuilder.newJob(TestQuartzJob).withIdentity(new JobKey("job", "group")).build() 50 | trigger = TriggerBuilder.newTrigger() 51 | .withIdentity(new TriggerKey("trigger", "group")) 52 | .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInMinutes(2).repeatForever()) 53 | .startNow() 54 | .build() 55 | 56 | scheduler.scheduleJob(job, trigger) 57 | } 58 | 59 | def cleanup() { 60 | scheduler.shutdown() 61 | } 62 | 63 | 64 | void 'build TriggerDescriptor correctly'() { 65 | when: 66 | TriggerDescriptor descriptor = 67 | TriggerDescriptor.build(JobDescriptor.build(job, scheduler), trigger, scheduler) 68 | then: 69 | descriptor.name == 'trigger' 70 | descriptor.group == 'group' 71 | descriptor.state == Trigger.TriggerState.NORMAL 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugins/quartz/TriggerUtils.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package grails.plugins.quartz 17 | 18 | import groovy.transform.CompileStatic 19 | import org.quartz.CronScheduleBuilder 20 | import org.quartz.CronTrigger 21 | import org.quartz.SimpleScheduleBuilder 22 | import org.quartz.SimpleTrigger 23 | import org.quartz.Trigger 24 | import org.quartz.TriggerBuilder 25 | 26 | /** 27 | * The util class which helps to build triggers for schedule methods. 28 | * 29 | * @author Vitalii Samolovskikh aka Kefir 30 | */ 31 | @CompileStatic 32 | class TriggerUtils { 33 | private static String generateTriggerName() { 34 | "GRAILS_" + UUID.randomUUID().toString() 35 | } 36 | 37 | static Trigger buildDateTrigger(String jobName, String jobGroup, Date scheduleDate) { 38 | return TriggerBuilder.newTrigger() 39 | .withIdentity(generateTriggerName(), GrailsJobClassConstants.DEFAULT_TRIGGERS_GROUP) 40 | .withPriority(6) 41 | .forJob(jobName, jobGroup) 42 | .startAt(scheduleDate) 43 | .build() 44 | } 45 | 46 | static SimpleTrigger buildSimpleTrigger(String jobName, String jobGroup, long repeatInterval, int repeatCount) { 47 | return TriggerBuilder.newTrigger() 48 | .withIdentity(generateTriggerName(), GrailsJobClassConstants.DEFAULT_TRIGGERS_GROUP) 49 | .withPriority(6) 50 | .forJob(jobName, jobGroup) 51 | .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInMilliseconds(repeatInterval).withRepeatCount(repeatCount)) 52 | .build() 53 | } 54 | 55 | static CronTrigger buildCronTrigger(String jobName, String jobGroup, String cronExpression) { 56 | return TriggerBuilder.newTrigger() 57 | .withIdentity(generateTriggerName(), GrailsJobClassConstants.DEFAULT_TRIGGERS_GROUP) 58 | .withPriority(6) 59 | .forJob(jobName, jobGroup) 60 | .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)) 61 | .build() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugins/quartz/GrailsJobClass.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grails.plugins.quartz; 18 | 19 | import grails.core.GrailsClass; 20 | 21 | import java.util.Map; 22 | 23 | /** 24 | * Represents a job class in Grails. 25 | * 26 | * @author Micha?? K??ujszo 27 | * @author Graeme Rocher 28 | * @author Marcel Overdijk 29 | * @author Sergey Nebolsin (nebolsin@gmail.com) 30 | * @since 0.1 31 | */ 32 | public interface GrailsJobClass extends GrailsClass { 33 | 34 | /** 35 | * Method which is executed by the job scheduler. 36 | */ 37 | public void execute(); 38 | 39 | /** 40 | * Get group name used for configuring scheduler. 41 | * 42 | * @return jobs group name for this job 43 | */ 44 | public String getGroup(); 45 | 46 | /** 47 | * If jobs can be executed concurrently returns true. 48 | * 49 | * @return true if several instances of this job can run concurrently 50 | */ 51 | public boolean isConcurrent(); 52 | 53 | /** 54 | * If job requires Hibernate Session bounded to thread returns true. 55 | * 56 | * @return true if this job require a Hibernate Session bounded to thread 57 | */ 58 | public boolean isSessionRequired(); 59 | 60 | /** 61 | * If job is durable returns true. 62 | * 63 | * @return true if this job is durable 64 | */ 65 | public boolean isDurability(); 66 | 67 | /** 68 | * If job should be re-executed if a 'recovery' or 'fail-over' situation is encountered returns true. 69 | * 70 | * @return true if this job requests recovery 71 | */ 72 | public boolean isRequestsRecovery(); 73 | 74 | /** 75 | * If job should be enabled or at all. Useful for testing new jobs and temporarily disabling jobs at the class property level 76 | * 77 | * @return true if this job is enabled 78 | */ 79 | public boolean isEnabled(); 80 | 81 | 82 | /** 83 | * Get job's description used for configuring job details. 84 | * 85 | * @return description for this job 86 | */ 87 | public String getDescription(); 88 | 89 | public Map getTriggers(); 90 | } 91 | -------------------------------------------------------------------------------- /etc/bin/test-reproducible-builds.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | # This file assumes the gnu version of coreutils is installed, which is not installed by default on a mac 20 | set -e 21 | 22 | export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) 23 | 24 | CWD=$(pwd) 25 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 26 | cd "${SCRIPT_DIR}/../.." 27 | 28 | rm -rf "${SCRIPT_DIR}/results" || true 29 | mkdir -p "${SCRIPT_DIR}/results" 30 | 31 | git clean -xdf --exclude='etc/bin' --exclude='.idea' --exclude='.gradle' 32 | killall -e java || true 33 | ./gradlew build --rerun-tasks -PskipTests --no-build-cache 34 | "${SCRIPT_DIR}/generate-build-artifact-hashes.groovy" > "${SCRIPT_DIR}/results/first.txt" 35 | mkdir -p "${SCRIPT_DIR}/results/first" 36 | find . -path ./etc -prune -o -type f -path '*/build/libs/*.jar' -print0 | xargs -0 cp --parents -t "${SCRIPT_DIR}/results/first/" 37 | 38 | git clean -xdf --exclude='etc/bin' --exclude='.idea' --exclude='.gradle' 39 | killall -e java || true 40 | ./gradlew build --rerun-tasks -PskipTests --no-build-cache 41 | "${SCRIPT_DIR}/generate-build-artifact-hashes.groovy" > "${SCRIPT_DIR}/results/second.txt" 42 | mkdir -p "${SCRIPT_DIR}/results/second" 43 | find . -path ./etc -prune -o -type f -path '*/build/libs/*.jar' -print0 | xargs -0 cp --parents -t "${SCRIPT_DIR}/results/second/" 44 | 45 | cd "${SCRIPT_DIR}/results" 46 | 47 | # diff -u first.txt second.txt 48 | DIFF_RESULTS=$(comm -3 first.txt second.txt | cut -d' ' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v '^$' | uniq | sort) 49 | echo "Differing artifacts:" 50 | echo "$DIFF_RESULTS" > diff.txt 51 | cat diff.txt 52 | 53 | printf '%s\n' "$DIFF_RESULTS" | sed 's|^etc/bin/results/||' > toPurge.txt 54 | find first -type f -name '*.jar' -print | sed 's|^first/||' | grep -F -x -v -f toPurge.txt | 55 | while IFS= read -r f; do 56 | rm -f "./first/$f" 57 | done 58 | find second -type f -name '*.jar' -print | sed 's|^second/||' | grep -F -x -v -f toPurge.txt | 59 | while IFS= read -r f; do 60 | rm -f "./second/$f" 61 | done 62 | rm toPurge.txt 63 | find . -type d -empty -delete 64 | cd "$CWD" 65 | -------------------------------------------------------------------------------- /etc/bin/verify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # 20 | set -euo pipefail 21 | 22 | PROJECT_NAME='grails-quartz' 23 | RELEASE_TAG=$1 24 | DOWNLOAD_LOCATION="${2:-.}" 25 | DOWNLOAD_LOCATION=$(realpath "${DOWNLOAD_LOCATION}") 26 | 27 | if [ -z "${RELEASE_TAG}" ]; then 28 | echo "Usage: $0 [release-tag] " 29 | exit 1 30 | fi 31 | 32 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 33 | CWD=$(pwd) 34 | VERSION=${RELEASE_TAG#v} 35 | 36 | cleanup() { 37 | echo "❌ Verification failed. ❌" 38 | } 39 | trap cleanup ERR 40 | 41 | cd "${DOWNLOAD_LOCATION}" 42 | 43 | echo "Downloading KEYS file ..." 44 | curl -sSfLO "https://dist.apache.org/repos/dist/release/grails/KEYS" 45 | echo "✅ KEYS Downloaded" 46 | 47 | echo "Downloading Artifacts ..." 48 | "${SCRIPT_DIR}/download-release-artifacts.sh" "${RELEASE_TAG}" "${DOWNLOAD_LOCATION}" 49 | echo "✅ Artifacts Downloaded" 50 | 51 | echo "Verifying Source Distribution ..." 52 | "${SCRIPT_DIR}/verify-source-distribution.sh" "${RELEASE_TAG}" "${DOWNLOAD_LOCATION}" 53 | echo "✅ Source Distribution Verified" 54 | 55 | echo "Verifying JAR Artifacts ..." 56 | "${SCRIPT_DIR}/verify-jar-artifacts.sh" "${RELEASE_TAG}" "${DOWNLOAD_LOCATION}" 57 | echo "✅ JAR Artifacts Verified" 58 | 59 | echo "Using Java at ..." 60 | which java 61 | java -version 62 | 63 | echo "Bootstrap Gradle ..." 64 | cd "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/gradle-bootstrap" 65 | 66 | if GRADLE_CMD="$(command -v gradlew 2>/dev/null)"; then 67 | : # found the wrapper on PATH 68 | elif GRADLE_CMD="$(command -v gradle 2>/dev/null)"; then 69 | : # fall back to system-wide Gradle 70 | else 71 | echo "ERROR: Neither gradlew nor gradle found on \$PATH." >&2 72 | exit 1 73 | fi 74 | ${GRADLE_CMD} 75 | echo "✅ Gradle Bootstrapped" 76 | 77 | echo "Applying License Audit ..." 78 | cd "${DOWNLOAD_LOCATION}/${PROJECT_NAME}" 79 | ./gradlew rat 80 | echo "✅ RAT passed" 81 | 82 | echo "Verifying Reproducible Build ..." 83 | set +e # because we have known issues here 84 | "${SCRIPT_DIR}/verify-reproducible.sh" "${DOWNLOAD_LOCATION}" 85 | set -e 86 | echo "✅ Reproducible Build Verified" 87 | 88 | echo "✅✅✅ Verification finished, see above instructions for remaining manual testing." 89 | 90 | cd "${CWD}" -------------------------------------------------------------------------------- /src/main/groovy/grails/plugins/quartz/GrailsJobClassConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grails.plugins.quartz; 18 | 19 | import org.quartz.SimpleTrigger; 20 | 21 | /** 22 | *

Holds plugin constants.

23 | * 24 | * @author Micha?? K??ujszo 25 | * @author Graeme Rocher 26 | * @author Marcel Overdijk 27 | * @author Sergey Nebolsin (nebolsin@gmail.com) 28 | * @see GrailsJobClass 29 | * @since 0.1 30 | */ 31 | public final class GrailsJobClassConstants { 32 | 33 | // restrict instantiation 34 | private GrailsJobClassConstants() {} 35 | 36 | public static final String EXECUTE = "execute"; 37 | 38 | public static final String INTERRUPT = "interrupt"; 39 | 40 | public static final String START_DELAY = "startDelay"; 41 | 42 | public static final String CRON_EXPRESSION = "cronExpression"; 43 | 44 | public static final String NAME = "name"; 45 | 46 | public static final String GROUP = "group"; 47 | 48 | public static final String DESCRIPTION = "description"; 49 | 50 | public static final String CONCURRENT = "concurrent"; 51 | 52 | public static final String SESSION_REQUIRED = "sessionRequired"; 53 | 54 | // TODO: deprecated, remove in the next release 55 | public static final String TIMEOUT = "timeout"; 56 | 57 | public static final String REPEAT_INTERVAL = "repeatInterval"; 58 | 59 | public static final String REPEAT_COUNT = "repeatCount"; 60 | 61 | public static final String DURABILITY = "durability"; 62 | 63 | public static final String REQUESTS_RECOVERY = "requestsRecovery"; 64 | 65 | public static final String ENABLED = "jobEnabled"; 66 | 67 | // Default values for Job's properties 68 | 69 | public static final long DEFAULT_REPEAT_INTERVAL = 60000l; // one minute 70 | 71 | public static final long DEFAULT_START_DELAY = 0l; // no delay by default 72 | 73 | public static final int DEFAULT_REPEAT_COUNT = SimpleTrigger.REPEAT_INDEFINITELY; 74 | 75 | public static final String DEFAULT_CRON_EXPRESSION = "0 0 6 * * ?"; 76 | 77 | public static final String DEFAULT_GROUP = "GRAILS_JOBS"; 78 | 79 | public static final String DEFAULT_DESCRIPTION = "Grails Job"; 80 | 81 | public static final boolean DEFAULT_CONCURRENT = true; 82 | 83 | public static final boolean DEFAULT_SESSION_REQUIRED = true; 84 | 85 | public static final String DEFAULT_TRIGGERS_GROUP = "GRAILS_TRIGGERS"; 86 | 87 | public static final boolean DEFAULT_DURABILITY = true; 88 | 89 | public static final boolean DEFAULT_REQUESTS_RECOVERY = false; 90 | 91 | public static final boolean DEFAULT_ENABLED = true; 92 | } 93 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | name: "Java CI" 17 | on: 18 | push: 19 | branches: 20 | - '[3-9]+.[0-9]+.x' 21 | pull_request: 22 | branches: 23 | - '[3-9]+.[0-9]+.x' 24 | workflow_dispatch: 25 | permissions: 26 | packages: read 27 | jobs: 28 | test_project: 29 | name: "Test Project" 30 | if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' 31 | runs-on: ubuntu-24.04 32 | steps: 33 | - name: "📥 Checkout repository" 34 | uses: actions/checkout@v4 35 | - name: "☕️ Setup JDK" 36 | uses: actions/setup-java@v4 37 | with: 38 | java-version: 17 39 | distribution: liberica 40 | - name: "🐘 Setup Gradle" 41 | uses: gradle/actions/setup-gradle@v4 42 | with: 43 | develocity-access-key: ${{ secrets.GRAILS_DEVELOCITY_ACCESS_KEY }} 44 | - name: "🏃‍♂️ Run Tests" 45 | run: ./gradlew check --continue 46 | publish_snapshot: 47 | name: "Build Project and Publish Snapshot" 48 | runs-on: ubuntu-24.04 49 | if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.repository_owner == 'apache' 50 | permissions: 51 | contents: write 52 | steps: 53 | - name: "📥 Checkout repository" 54 | uses: actions/checkout@v4 55 | - name: "☕️ Setup JDK" 56 | uses: actions/setup-java@v4 57 | with: 58 | java-version: 17 59 | distribution: liberica 60 | - name: "🐘 Setup Gradle" 61 | uses: gradle/actions/setup-gradle@v4 62 | with: 63 | develocity-access-key: ${{ secrets.GRAILS_DEVELOCITY_ACCESS_KEY }} 64 | - name: "📤 Publish Snapshot artifacts" 65 | env: 66 | GITHUB_MAVEN_PASSWORD: ${{ secrets.GITHUB_TOKEN }} 67 | GRAILS_PUBLISH_RELEASE: 'false' 68 | MAVEN_PUBLISH_USERNAME: ${{ secrets.NEXUS_USER }} 69 | MAVEN_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PW }} 70 | MAVEN_PUBLISH_URL: ${{ secrets.GRAILS_NEXUS_PUBLISH_SNAPSHOT_URL }} 71 | run: ./gradlew --no-build-cache publish 72 | - name: "🔨 Generate Snapshot Documentation" 73 | run: ./gradlew docs 74 | - name: "🚀 Publish to Github Pages" 75 | uses: apache/grails-github-actions/deploy-github-pages@asf 76 | env: 77 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | GRADLE_PUBLISH_RELEASE: 'false' 79 | SOURCE_FOLDER: build/docs 80 | -------------------------------------------------------------------------------- /etc/bin/verify-source-distribution.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # 20 | set -euo pipefail 21 | 22 | PROJECT_NAME='grails-quartz' 23 | RELEASE_TAG=$1 24 | DOWNLOAD_LOCATION="${2:-downloads}" 25 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 26 | 27 | if [ -z "${RELEASE_TAG}" ]; then 28 | echo "Usage: $0 [release-tag] " 29 | exit 1 30 | fi 31 | 32 | VERSION=${RELEASE_TAG#v} 33 | 34 | cd "${DOWNLOAD_LOCATION}" 35 | ZIP_FILE=$(ls "apache-${PROJECT_NAME}-${VERSION}-src.zip" 2>/dev/null | head -n 1) 36 | 37 | if [ -z "${ZIP_FILE}" ]; then 38 | echo "Error: Could not find apache-${PROJECT_NAME}-${VERSION}-src.zip in ${DOWNLOAD_LOCATION}" 39 | exit 1 40 | fi 41 | 42 | export GRAILS_GPG_HOME=$(mktemp -d) 43 | cleanup() { 44 | rm -rf "${GRAILS_GPG_HOME}" 45 | } 46 | trap cleanup EXIT 47 | 48 | echo "Verifying checksum..." 49 | shasum -a 512 -c "apache-${PROJECT_NAME}-${VERSION}-src.zip.sha512" 50 | echo "✅ Checksum Verified" 51 | 52 | echo "Importing GPG key to independent GPG home ..." 53 | gpg --homedir "${GRAILS_GPG_HOME}" --import "${DOWNLOAD_LOCATION}/KEYS" 54 | echo "✅ GPG Key Imported" 55 | 56 | echo "Verifying GPG signature..." 57 | gpg --homedir "${GRAILS_GPG_HOME}" --verify "apache-${PROJECT_NAME}-${VERSION}-src.zip.asc" "apache-${PROJECT_NAME}-${VERSION}-src.zip" 58 | echo "✅ GPG Verified" 59 | 60 | SRC_DIR="${PROJECT_NAME}" 61 | 62 | if [ -d "${SRC_DIR}" ]; then 63 | echo "Previous ${SRC_DIR} directory found, purging" 64 | cd "${SRC_DIR}" 65 | find . -mindepth 1 -path ./etc -prune -o -exec rm -rf {} + 66 | cd etc 67 | find . -mindepth 1 -path ./bin -prune -o -exec rm -rf {} + 68 | cd bin 69 | find . -mindepth 1 -path ./results -prune -o -exec rm -rf {} + 70 | cd "${DOWNLOAD_LOCATION}" 71 | fi 72 | echo "Extracting zip file..." 73 | unzip -q "apache-${PROJECT_NAME}-${VERSION}-src.zip" 74 | 75 | if [ ! -d "${SRC_DIR}" ]; then 76 | echo "Error: Expected extracted folder '${SRC_DIR}' not found." 77 | exit 1 78 | fi 79 | 80 | echo "Checking for required files existence..." 81 | REQUIRED_FILES=("LICENSE" "NOTICE" "README.md" "PUBLISHED_ARTIFACTS" "CHECKSUMS" "BUILD_DATE") 82 | 83 | for FILE in "${REQUIRED_FILES[@]}"; do 84 | if [ ! -f "${SRC_DIR}/$FILE" ]; then 85 | echo "❌ Missing required file: $FILE" 86 | exit 1 87 | fi 88 | 89 | echo "✅ Found required file: $FILE" 90 | done 91 | 92 | echo "✅ All source distribution checks passed successfully for ${VERSION}." -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /.github/scripts/releaseDistributions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Licensed to the Apache Software Foundation (ASF) under one 5 | # or more contributor license agreements. See the NOTICE file 6 | # distributed with this work for additional information 7 | # regarding copyright ownership. The ASF licenses this file 8 | # to you under the Apache License, Version 2.0 (the 9 | # "License"); you may not use this file except in compliance 10 | # with the License. You may obtain a copy of the License at 11 | # 12 | # https://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, 15 | # software distributed under the License is distributed on an 16 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | # KIND, either express or implied. See the License for the 18 | # specific language governing permissions and limitations 19 | # under the License. 20 | # 21 | 22 | # ./releaseDistributions.sh 23 | 24 | set -euo pipefail 25 | 26 | if [[ $# -ne 3 ]]; then 27 | echo "Usage: $0 " >&2 28 | exit 1 29 | fi 30 | 31 | RELEASE_TAG="$1" 32 | RELEASE_VERSION="${RELEASE_TAG#v}" 33 | SVN_FOLDER="$2" 34 | SVN_USER="$3" 35 | RELEASE_ROOT="https://dist.apache.org/repos/dist/release/grails/${SVN_FOLDER}" 36 | DEV_ROOT="https://dist.apache.org/repos/dist/dev/grails/${SVN_FOLDER}" 37 | 38 | read -r -s -p "Password: " SVN_PASS 39 | echo 40 | 41 | if [[ -z "${RELEASE_TAG}" ]]; then 42 | echo "❌ ERROR: Release Tag must not be empty." >&2 43 | exit 1 44 | fi 45 | if [[ -z "${SVN_FOLDER}" ]]; then 46 | echo "❌ ERROR: SVN folder name must not be empty." >&2 47 | exit 1 48 | fi 49 | if [[ -z "${SVN_USER}" ]]; then 50 | echo "❌ ERROR: Username must not be empty." >&2 51 | exit 1 52 | fi 53 | if [[ -z "${SVN_PASS}" ]]; then 54 | echo "❌ ERROR: Password must not be empty." >&2 55 | exit 1 56 | fi 57 | 58 | svn_flags=(--non-interactive --trust-server-cert --username "${SVN_USER}" --password "${SVN_PASS}") 59 | 60 | svn_exists() { 61 | local url="$1" 62 | svn ls "${svn_flags[@]}" --depth=empty "${url}" >/dev/null 2>&1 63 | } 64 | 65 | old_release_folder="$(svn ls "${svn_flags[@]}" "${RELEASE_ROOT}" | awk -F/ 'NF{print $1; exit}')" 66 | if [[ -n "${old_release_folder}" ]]; then 67 | PRIOR_RELEASE_URL="${RELEASE_ROOT}/${old_release_folder}" 68 | echo "🗑️ Deleting old release folder: ${PRIOR_RELEASE_URL}" 69 | svn rm "${svn_flags[@]}" -m "Remove previous release ${old_release_folder}" "${PRIOR_RELEASE_URL}" 70 | echo "✅ Deleted old release folder" 71 | else 72 | echo "ℹ️ No existing release subfolder found under ${RELEASE_ROOT}" 73 | fi 74 | 75 | DEV_VERSION_URL="$DEV_ROOT/${RELEASE_VERSION}" 76 | RELEASE_VERSION_URL="$RELEASE_ROOT/${RELEASE_VERSION}" 77 | 78 | if ! svn_exists "${DEV_VERSION_URL}"; then 79 | echo "❌ ERROR: dev folder for ${RELEASE_VERSION} does not exist at: ${DEV_VERSION_URL}" >&2 80 | exit 2 81 | fi 82 | 83 | if svn_exists "${RELEASE_VERSION_URL}"; then 84 | echo "❌ ERROR: release folder for ${RELEASE_VERSION} already exists at: ${RELEASE_VERSION_URL}" >&2 85 | exit 3 86 | fi 87 | 88 | echo "🚀 Promoting ${DEV_VERSION_URL} -> ${RELEASE_VERSION_URL}" 89 | svn mv "${svn_flags[@]}" -m "Promote Apache Grails ${RELEASE_VERSION} from dev to release" "${DEV_VERSION_URL}" "${RELEASE_VERSION_URL}" 90 | echo "✅ Promoted" 91 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugins/quartz/JobArtefactHandler.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grails.plugins.quartz 18 | 19 | import grails.core.ArtefactHandlerAdapter 20 | import org.codehaus.groovy.ast.ClassNode 21 | import org.grails.compiler.injection.GrailsASTUtils 22 | import org.quartz.JobExecutionContext 23 | import org.springframework.util.ReflectionUtils 24 | 25 | import java.lang.reflect.Method 26 | import java.util.regex.Pattern 27 | 28 | import static org.grails.io.support.GrailsResourceUtils.GRAILS_APP_DIR 29 | import static org.grails.io.support.GrailsResourceUtils.REGEX_FILE_SEPARATOR 30 | 31 | /** 32 | * Grails artifact handler for job classes. 33 | * 34 | * @author Marc Palmer (marc@anyware.co.uk) 35 | * @author Sergey Nebolsin (nebolsin@gmail.com) 36 | * @since 0.1 37 | */ 38 | public class JobArtefactHandler extends ArtefactHandlerAdapter { 39 | 40 | static final String TYPE = "Job" 41 | public static Pattern JOB_PATH_PATTERN = Pattern.compile(".+" + REGEX_FILE_SEPARATOR + GRAILS_APP_DIR + REGEX_FILE_SEPARATOR + "jobs" + REGEX_FILE_SEPARATOR + "(.+)\\.(groovy)"); 42 | 43 | public JobArtefactHandler() { 44 | super(TYPE, GrailsJobClass.class, DefaultGrailsJobClass.class, TYPE) 45 | } 46 | 47 | boolean isArtefact(ClassNode classNode) { 48 | if(classNode == null || 49 | !isValidArtefactClassNode(classNode, classNode.getModifiers()) || 50 | !classNode.getName().endsWith(DefaultGrailsJobClass.JOB) || 51 | !classNode.getMethods(GrailsJobClassConstants.EXECUTE)) { 52 | return false 53 | } 54 | 55 | URL url = GrailsASTUtils.getSourceUrl(classNode) 56 | 57 | url && JOB_PATH_PATTERN.matcher(url.getFile()).find() 58 | } 59 | 60 | boolean isArtefactClass(Class clazz) { 61 | // class shouldn't be null and should ends with Job suffix 62 | if (clazz == null || !clazz.getName().endsWith(DefaultGrailsJobClass.JOB)) return false 63 | // and should have one of execute() or execute(JobExecutionContext) methods defined 64 | Method method = ReflectionUtils.findMethod(clazz, GrailsJobClassConstants.EXECUTE) 65 | if (method == null) { 66 | // we're using Object as a param here to allow groovy-style 'def execute(param)' method 67 | method = ReflectionUtils.findMethod(clazz, GrailsJobClassConstants.EXECUTE, [Object] as Class[]) 68 | } 69 | if (method == null) { 70 | // also check for the execution context as a variable because that's what's being passed 71 | method = ReflectionUtils.findMethod(clazz, GrailsJobClassConstants.EXECUTE, [JobExecutionContext] as Class[]) 72 | } 73 | method != null 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /etc/bin/generate-build-artifact-hashes.groovy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | /* 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. See the NOTICE file 5 | * distributed with this work for additional information 6 | * regarding copyright ownership. The ASF licenses this file 7 | * to you under the Apache License, Version 2.0 (the 8 | * "License"); you may not use this file except in compliance 9 | * with the License. You may obtain a copy of the License at 10 | * 11 | * https://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, 14 | * software distributed under the License is distributed on an 15 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | * KIND, either express or implied. See the License for the 17 | * specific language governing permissions and limitations 18 | * under the License. 19 | */ 20 | import java.nio.file.* 21 | import java.security.MessageDigest 22 | 23 | // --------------------------------------------------------------------------- 24 | String sha512(Path file) { 25 | MessageDigest md = MessageDigest.getInstance('SHA-512') 26 | file.withInputStream { is -> 27 | byte[] buf = new byte[8192] 28 | for (int r = is.read(buf); r > 0; r = is.read(buf)) 29 | md.update(buf, 0, r) 30 | } 31 | md.digest().collect { String.format('%02x', it) }.join() 32 | } 33 | 34 | Path scriptDir = Paths.get(getClass() 35 | .protectionDomain 36 | .codeSource 37 | .location 38 | .toURI()) 39 | .toAbsolutePath() 40 | .parent 41 | 42 | 43 | Path root = scriptDir.resolve('..').resolve('..').normalize() 44 | if(args && args.length > 0) { 45 | System.out.println("Finding jars in: ${args[0]}" as String) 46 | root = Paths.get(args[0]).toAbsolutePath().normalize() 47 | } 48 | 49 | // --------------------------------------------------------------------------- 50 | // Decide where to search: project root by default, or user-supplied path 51 | // (absolute or relative to project root) when an argument is given. 52 | Path scanRoot 53 | if (this.args && this.args.length > 0) { 54 | Path argPath = Paths.get(this.args[0]) 55 | scanRoot = argPath.isAbsolute() ? argPath : root.resolve(argPath).normalize() 56 | if (!Files.exists(scanRoot)) { 57 | System.err.println "❌ Path '${scanRoot}' does not exist." 58 | System.exit(1) 59 | } 60 | } else { 61 | scanRoot = root 62 | } 63 | List artifacts = [] 64 | Files.walk(scanRoot) 65 | .filter { 66 | Files.isRegularFile(it) && 67 | !it.toString().contains("buildSrc") && 68 | !it.toString().contains("etc") && 69 | it.toString().endsWith('.jar') && 70 | it.toString().contains("${File.separator}build${File.separator}libs${File.separator}" as String) 71 | } 72 | .forEach { artifacts << it } 73 | 74 | artifacts.findAll { 75 | !it.toString().contains("${File.separator}buildSrc${File.separator}" as String) // build src jars aren't published 76 | !it.toString().contains("${File.separator}examples${File.separator}" as String) // test examples aren't published 77 | }.sort { a, b -> a.toString() <=> b.toString() 78 | }.collect { Path jar -> 79 | String hash = sha512(jar) 80 | String relative = root.relativize(jar).toString() 81 | "${relative} ${hash}" 82 | }.sort().each { 83 | println it 84 | } -------------------------------------------------------------------------------- /src/main/groovy/grails/plugins/quartz/listeners/SessionBinderJobListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grails.plugins.quartz.listeners; 18 | 19 | import grails.persistence.support.PersistenceContextInterceptor; 20 | import org.quartz.JobExecutionContext; 21 | import org.quartz.JobExecutionException; 22 | import org.quartz.listeners.JobListenerSupport; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | /** 27 | * JobListener implementation which wraps the execution of a Quartz Job in a 28 | * persistence context, via the persistenceInterceptor. 29 | * 30 | * @author Sergey Nebolsin (nebolsin@gmail.com) 31 | * @since 0.2 32 | */ 33 | public class SessionBinderJobListener extends JobListenerSupport { 34 | 35 | private static final Logger LOG = LoggerFactory.getLogger(SessionBinderJobListener.class); 36 | 37 | public static final String NAME = "sessionBinderListener"; 38 | 39 | private PersistenceContextInterceptor persistenceInterceptor; 40 | 41 | public String getName() { 42 | return NAME; 43 | } 44 | 45 | /** 46 | * It is used by the Spring to inject a persistence interceptor. 47 | * @return the reference of the currently active bean implementation of persistenceInterceptor 48 | */ 49 | @SuppressWarnings("UnusedDeclaration") 50 | public PersistenceContextInterceptor getPersistenceInterceptor() { 51 | return persistenceInterceptor; 52 | } 53 | 54 | /** 55 | * It is used by the Spring to inject a persistence interceptor. 56 | * @param persistenceInterceptor - Normally applied by bean injection to set the reference to the persistenceInterceptor 57 | */ 58 | @SuppressWarnings("UnusedDeclaration") 59 | public void setPersistenceInterceptor(PersistenceContextInterceptor persistenceInterceptor) { 60 | this.persistenceInterceptor = persistenceInterceptor; 61 | } 62 | 63 | /** 64 | * Before job executing. Init persistence context. 65 | */ 66 | public void jobToBeExecuted(JobExecutionContext context) { 67 | if (persistenceInterceptor != null) { 68 | persistenceInterceptor.init(); 69 | LOG.debug("Persistence session is opened."); 70 | } 71 | } 72 | 73 | /** 74 | * After job executing. Flush and destroy persistence context. 75 | */ 76 | public void jobWasExecuted(JobExecutionContext context, JobExecutionException exception) { 77 | if (persistenceInterceptor != null) { 78 | try { 79 | persistenceInterceptor.flush(); 80 | persistenceInterceptor.clear(); 81 | LOG.debug("Persistence session is flushed."); 82 | } catch (Exception e) { 83 | LOG.error("Failed to flush session after job: " + context.getJobDetail().getDescription(), e); 84 | } finally { 85 | try { 86 | persistenceInterceptor.destroy(); 87 | } catch (Exception e) { 88 | LOG.error("Failed to finalize session after job: " + context.getJobDetail().getDescription(), e); 89 | } 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugins/quartz/DefaultGrailsJobClassSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grails.plugins.quartz 18 | 19 | import spock.lang.Specification 20 | 21 | class DefaultGrailsJobClassSpec extends Specification { 22 | protected GroovyClassLoader gcl = new GroovyClassLoader() 23 | 24 | def cleanup() { 25 | gcl.clearCache() 26 | } 27 | 28 | void 'default properties are set correctly'() { 29 | setup: 30 | def jobClass = gcl.parseClass('class TestJob { def execute(){} }') 31 | def grailsJobClass = new DefaultGrailsJobClass(jobClass) 32 | expect: 33 | assert 'GRAILS_JOBS' == grailsJobClass.group: "Wrong default group" 34 | assert grailsJobClass.sessionRequired: "Job should require Hibernate session by default" 35 | assert grailsJobClass.concurrent: "Job should be concurrent by default" 36 | } 37 | 38 | void 'job class execute method works correctly'() { 39 | setup: 40 | boolean wasExecuted = false 41 | def testClosure = { wasExecuted = true } 42 | Class jobClass = gcl.parseClass(""" 43 | class TestJob { 44 | static testClosure 45 | def execute() { 46 | testClosure.call() 47 | } 48 | } 49 | """.stripIndent()) 50 | GrailsJobClass grailsJobClass = new DefaultGrailsJobClass(jobClass) 51 | grailsJobClass.referenceInstance.testClosure = testClosure 52 | when: 53 | grailsJobClass.execute() 54 | then: 55 | assert wasExecuted: "Job wasn't executed" 56 | } 57 | 58 | void 'session required parameter is handled correctly'() { 59 | setup: 60 | Class jobClass = gcl.parseClass(""" 61 | class TestJob { 62 | static sessionRequired = false 63 | def execute() {} 64 | } 65 | """.stripIndent()) 66 | GrailsJobClass grailsJobClass = new DefaultGrailsJobClass(jobClass) 67 | expect: 68 | assert !grailsJobClass.sessionRequired: "Hibernate Session shouldn't be required" 69 | } 70 | 71 | void 'concurrent parameter is handled correctly'() { 72 | setup: 73 | Class jobClass = gcl.parseClass(""" 74 | class TestJob { 75 | static concurrent = false 76 | def execute() {} 77 | } 78 | """.stripIndent()) 79 | GrailsJobClass grailsJobClass = new DefaultGrailsJobClass(jobClass) 80 | expect: 81 | assert !grailsJobClass.concurrent: "Job class shouldn't be marked as concurrent" 82 | } 83 | 84 | void 'group parameter is handled correctly'() { 85 | setup: 86 | Class jobClass = gcl.parseClass(""" 87 | class TestJob { 88 | static group = 'myGroup' 89 | def execute() {} 90 | } 91 | """.stripIndent()) 92 | GrailsJobClass grailsJobClass = new DefaultGrailsJobClass(jobClass) 93 | expect: 94 | 'myGroup' == grailsJobClass.group 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/docs/scheduling.adoc: -------------------------------------------------------------------------------- 1 | //// 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | //// 19 | 20 | [[scheduling]] 21 | == Scheduling Basics 22 | 23 | === Scheduling Jobs 24 | 25 | To create a new job, run the `grails create-job` command and enter the name of the job. Grails will create a new job and place it in the `grails-app/jobs` directory: 26 | 27 | [source,groovy] 28 | ---- 29 | class MyJob { 30 | 31 | static group = 'MyGroup' 32 | static description = 'Example job with Simple Trigger' 33 | static triggers = { 34 | simple( 35 | name: 'mySimpleTrigger', 36 | startDelay: 60000, 37 | repeatInterval: 1000 38 | ) 39 | } 40 | 41 | void execute() { 42 | println 'Job run!' 43 | } 44 | } 45 | ---- 46 | 47 | The above example will wait for 1 minute and after that will call the `execute()` method every second. The `repeatInterval` and `startDelay` properties are specified in milliseconds and must have Integer or Long type. If these properties are not specified, default values are applied (1 minute for `repeatInterval` property and 30 seconds for `startDelay` property). Jobs can optionally be placed in different groups. 48 | The trigger name property must be unique across all triggers in the application. 49 | 50 | By default, jobs will not be executed when running under the test environment. 51 | 52 | 53 | ==== Scheduling a Cron Job 54 | 55 | Jobs can be scheduled using a cron expression. For those unfamiliar with `cron`, this means being able to create a firing schedule such as "At 8:00am every Monday through Friday" or "At 1:30am every last Friday of the month". (See the API docs for the `CronTrigger` class in Quartz for more info on cron expressions). 56 | 57 | [source,groovy] 58 | ---- 59 | class MyJob { 60 | 61 | static group = 'MyGroup' 62 | static description = 'Example job with Cron Trigger' 63 | static triggers = { 64 | cron( 65 | name: 'myTrigger', 66 | cronExpression: '0 0 6 * * ?' 67 | ) 68 | } 69 | 70 | void execute() { 71 | println 'Job run!' 72 | } 73 | } 74 | ---- 75 | 76 | The fields in the cronExpression are: (summarizing the Quartz CronTrigger Tutorial) 77 | 78 | [,] 79 | ---- 80 | cronExpression: "s m h D M W Y" 81 | | | | | | | - Year <> 82 | | | | | | - Day of Week, 1-7 or SUN-SAT, ? 83 | | | | | - Month, 1-12 or JAN-DEC 84 | | | | - Day of Month, 1-31, ? 85 | | | - Hour, 0-23 86 | | - Minute, 0-59 87 | - Second, 0-59 88 | ---- 89 | 90 | [NOTE] 91 | ==== 92 | * Year is the only optional field and may be omitted, the rest are mandatory. 93 | * Day-of-Week and Month are case-insensitive, so "DEC" = "dec" = "Dec" 94 | * Either Day-of-Week or Day-of-Month must be "?", or you will get an error since support by the underlying library is not complete. So you can't specify both fields, nor leave both as the all-values wildcard "*"; this is a departure from the unix crontab specification. 95 | * See the CronTrigger Tutorial for an explanation of all the special characters you may use. 96 | ==== -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | name-template: $RESOLVED_VERSION 17 | tag-template: v$RESOLVED_VERSION 18 | pull-request: 19 | title-templates: 20 | fix: '🐛 $TITLE (#$NUMBER)' 21 | feat: '🚀 $TITLE (#$NUMBER)' 22 | default: '$TITLE (#$NUMBER)' 23 | autolabeler: 24 | - label: 'bug' 25 | branch: 26 | - '/fix\/.+/' 27 | title: 28 | - '/fix/i' 29 | - label: 'improvement' 30 | branch: 31 | - '/improv\/.+/' 32 | title: 33 | - '/improv/i' 34 | - label: 'feature' 35 | branch: 36 | - '/feature\/.+/' 37 | title: 38 | - '/feat/i' 39 | - label: 'documentation' 40 | branch: 41 | - '/docs\/.+/' 42 | title: 43 | - '/docs/i' 44 | - label: 'maintenance' 45 | branch: 46 | - '/(chore|refactor|style|test|ci|perf|build)\/.+/' 47 | title: 48 | - '/(chore|refactor|style|test|ci|perf|build)/i' 49 | - label: 'chore' 50 | branch: 51 | - '/chore\/.+/' 52 | title: 53 | - '/chore/i' 54 | - label: 'refactor' 55 | branch: 56 | - '/refactor\/.+/' 57 | title: 58 | - '/refactor/i' 59 | - label: 'style' 60 | branch: 61 | - '/style\/.+/' 62 | title: 63 | - '/style/i' 64 | - label: 'test' 65 | branch: 66 | - '/test\/.+/' 67 | title: 68 | - '/test/i' 69 | - label: 'ci' 70 | branch: 71 | - '/ci\/.+/' 72 | title: 73 | - '/ci/i' 74 | - label: 'perf' 75 | branch: 76 | - '/perf\/.+/' 77 | title: 78 | - '/perf/i' 79 | - label: 'build' 80 | branch: 81 | - '/build\/.+/' 82 | title: 83 | - '/build/i' 84 | - label: 'deps' 85 | branch: 86 | - '/deps\/.+/' 87 | title: 88 | - '/deps/i' 89 | - label: 'revert' 90 | branch: 91 | - '/revert\/.+/' 92 | title: 93 | - '/revert/i' 94 | categories: 95 | - title: '🚀 Features' 96 | labels: 97 | - 'feature' 98 | - "type: enhancement" 99 | - "type: new feature" 100 | - "type: major" 101 | - "type: minor" 102 | - title: '💡 Improvements' 103 | labels: 104 | - 'improvement' 105 | - "type: improvement" 106 | 107 | - title: '🐛 Bug Fixes' 108 | labels: 109 | - 'fix' 110 | - 'bug' 111 | - "type: bug" 112 | - title: '📚 Documentation' 113 | labels: 114 | - 'docs' 115 | - title: '🔧 Maintenance' 116 | labels: 117 | - 'maintenance' 118 | - 'chore' 119 | - 'refactor' 120 | - 'style' 121 | - 'test' 122 | - 'ci' 123 | - 'perf' 124 | - 'build' 125 | - "type: ci" 126 | - "type: build" 127 | - title: '⏪ Reverts' 128 | labels: 129 | - 'revert' 130 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 131 | version-resolver: 132 | major: 133 | labels: 134 | - 'type: major' 135 | minor: 136 | labels: 137 | - 'type: minor' 138 | patch: 139 | labels: 140 | - 'type: patch' 141 | default: patch 142 | template: | 143 | ## What's Changed 144 | 145 | $CHANGES 146 | 147 | ## Contributors 148 | 149 | $CONTRIBUTORS -------------------------------------------------------------------------------- /src/main/groovy/grails/plugins/quartz/QuartzJob.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package grails.plugins.quartz 17 | 18 | import grails.core.GrailsApplication 19 | import grails.core.support.GrailsApplicationAware 20 | import groovy.transform.CompileDynamic 21 | import groovy.transform.CompileStatic 22 | import org.quartz.JobDataMap 23 | import org.quartz.JobKey 24 | import org.quartz.Scheduler 25 | import org.quartz.SimpleTrigger 26 | import org.quartz.Trigger 27 | import org.quartz.TriggerKey 28 | import org.quartz.spi.MutableTrigger 29 | import org.springframework.util.Assert 30 | 31 | @CompileStatic 32 | trait QuartzJob implements GrailsApplicationAware { 33 | private static Scheduler internalScheduler 34 | private static GrailsJobClass internalJobArtefact 35 | 36 | GrailsApplication grailsApplication 37 | 38 | static triggerNow(Map params = null) { 39 | internalScheduler.triggerJob(new JobKey(this.getName(), internalJobArtefact.group), params ? new JobDataMap(params) : null) 40 | } 41 | 42 | @CompileDynamic 43 | static schedule(Long repeatInterval, Integer repeatCount = SimpleTrigger.REPEAT_INDEFINITELY, Map params = null) { 44 | internalScheduleTrigger(TriggerUtils.buildSimpleTrigger(this.getName(), internalJobArtefact.group, repeatInterval, repeatCount), params) 45 | } 46 | 47 | @CompileDynamic 48 | static schedule(Date scheduleDate, Map params = null) { 49 | internalScheduleTrigger(TriggerUtils.buildDateTrigger(this.getName(), internalJobArtefact.group, scheduleDate), params) 50 | } 51 | 52 | @CompileDynamic 53 | static schedule(String cronExpression, Map params = null) { 54 | internalScheduleTrigger(TriggerUtils.buildCronTrigger(this.getName(), internalJobArtefact.group, cronExpression), params) 55 | } 56 | 57 | static schedule(Trigger trigger, Map params = null) { 58 | def jobKey = new JobKey(this.getName(), internalJobArtefact.group) 59 | Assert.isTrue trigger.jobKey == jobKey || (trigger instanceof MutableTrigger), 60 | "The trigger job key is not equal to the job key or the trigger is immutable" 61 | 62 | ((MutableTrigger)trigger).jobKey = jobKey 63 | 64 | if (params) { 65 | trigger.jobDataMap.putAll(params) 66 | } 67 | internalScheduler.scheduleJob(trigger) 68 | } 69 | 70 | static removeJob() { 71 | internalScheduler.deleteJob(new JobKey(this.getName(), internalJobArtefact.group)) 72 | } 73 | 74 | static reschedule(Trigger trigger, Map params = null) { 75 | if (params) trigger.jobDataMap.putAll(params) 76 | internalScheduler.rescheduleJob(trigger.key, trigger) 77 | } 78 | 79 | static unschedule(String triggerName, String triggerGroup = GrailsJobClassConstants.DEFAULT_TRIGGERS_GROUP) { 80 | internalScheduler.unscheduleJob(TriggerKey.triggerKey(triggerName, triggerGroup)) 81 | } 82 | 83 | private static internalScheduleTrigger(Trigger trigger, Map params = null) { 84 | if (params) { 85 | trigger.jobDataMap.putAll(params) 86 | } 87 | internalScheduler.scheduleJob(trigger) 88 | } 89 | 90 | public static setScheduler(Scheduler scheduler) { 91 | internalScheduler = scheduler 92 | } 93 | 94 | public static setGrailsJobClass(GrailsJobClass gjc) { 95 | internalJobArtefact = gjc 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugins/quartz/JobDetailFactoryBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grails.plugins.quartz; 18 | 19 | import org.quartz.JobDetail; 20 | import org.springframework.beans.factory.FactoryBean; 21 | import org.springframework.beans.factory.InitializingBean; 22 | 23 | import static org.quartz.JobBuilder.newJob; 24 | 25 | /** 26 | * Simplified version of Spring's MethodInvokingJobDetailFactoryBean 27 | * that avoids issues with non-serializable classes (for JDBC storage). 28 | * 29 | * @author Burt Beckwith 30 | * @author Sergey Nebolsin (nebolsin@gmail.com) 31 | * @since 0.3.2 32 | */ 33 | public class JobDetailFactoryBean implements FactoryBean, InitializingBean { 34 | public static final transient String JOB_NAME_PARAMETER = "org.grails.plugins.quartz.grailsJobName"; 35 | 36 | // Properties 37 | private GrailsJobClass jobClass; 38 | 39 | // Returned object 40 | private JobDetail jobDetail; 41 | 42 | 43 | public void setJobClass(GrailsJobClass jobClass) { 44 | this.jobClass = jobClass; 45 | } 46 | 47 | /** 48 | * {@inheritDoc} 49 | * 50 | * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 51 | */ 52 | public void afterPropertiesSet() { 53 | String name = jobClass.getFullName(); 54 | if (name == null) { 55 | throw new IllegalStateException("name is required"); 56 | } 57 | 58 | String group = jobClass.getGroup(); 59 | if (group == null) { 60 | throw new IllegalStateException("group is required"); 61 | } 62 | 63 | // Consider the concurrent flag to choose between stateful and stateless job. 64 | Class clazz = 65 | jobClass.isConcurrent() ? GrailsJobFactory.GrailsJob.class : GrailsJobFactory.StatefulGrailsJob.class; 66 | 67 | // Build JobDetail instance. 68 | jobDetail = 69 | newJob(clazz) 70 | .withIdentity(name, group) 71 | .storeDurably(jobClass.isDurability()) 72 | .requestRecovery(jobClass.isRequestsRecovery()) 73 | .usingJobData(JOB_NAME_PARAMETER, name) 74 | .withDescription(jobClass.getDescription()) 75 | .build(); 76 | } 77 | 78 | /** 79 | * {@inheritDoc} 80 | * 81 | * @see org.springframework.beans.factory.FactoryBean#getObject() 82 | */ 83 | @Override 84 | public JobDetail getObject() { 85 | return jobDetail; 86 | } 87 | 88 | /** 89 | * {@inheritDoc} 90 | * 91 | * @see org.springframework.beans.factory.FactoryBean#getObjectType() 92 | */ 93 | @Override 94 | public Class getObjectType() { 95 | return JobDetail.class; 96 | } 97 | 98 | /** 99 | * {@inheritDoc} 100 | * 101 | * @see org.springframework.beans.factory.FactoryBean#isSingleton() 102 | */ 103 | @Override 104 | public boolean isSingleton() { 105 | return true; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /etc/bin/download-release-artifacts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # 20 | set -e 21 | 22 | PROJECT_NAME='grails-quartz' 23 | REPO_NAME='apache/grails-quartz' 24 | SVN_FOLDER='quartz' 25 | RELEASE_TAG=$1 26 | DOWNLOAD_LOCATION="${2:-downloads}" 27 | 28 | if [ -z "${RELEASE_TAG}" ]; then 29 | echo "Usage: $0 [release-tag] " 30 | exit 1 31 | fi 32 | 33 | echo "Downloading files to ${DOWNLOAD_LOCATION}" 34 | mkdir -p "${DOWNLOAD_LOCATION}" 35 | 36 | VERSION=${RELEASE_TAG#v} 37 | 38 | # Source distro 39 | echo "Downloading GitHub Release files" 40 | curl -f -L -o "${DOWNLOAD_LOCATION}/github-apache-${PROJECT_NAME}-${VERSION}-src.zip" "https://github.com/${REPO_NAME}/releases/download/${RELEASE_TAG}/apache-${PROJECT_NAME}-${VERSION}-src.zip" 41 | curl -f -L -o "${DOWNLOAD_LOCATION}/github-apache-${PROJECT_NAME}-${VERSION}-src.zip.asc" "https://github.com/${REPO_NAME}/releases/download/${RELEASE_TAG}/apache-${PROJECT_NAME}-${VERSION}-src.zip.asc" 42 | curl -f -L -o "${DOWNLOAD_LOCATION}/github-apache-${PROJECT_NAME}-${VERSION}-src.zip.sha512" "https://github.com/${REPO_NAME}/releases/download/${RELEASE_TAG}/apache-${PROJECT_NAME}-${VERSION}-src.zip.sha512" 43 | 44 | echo "Downloading SVN Release files" 45 | curl -f -L -o "${DOWNLOAD_LOCATION}/apache-${PROJECT_NAME}-${VERSION}-src.zip" "https://dist.apache.org/repos/dist/dev/grails/${SVN_FOLDER}/${VERSION}/sources/apache-${PROJECT_NAME}-${VERSION}-src.zip" 46 | curl -f -L -o "${DOWNLOAD_LOCATION}/apache-${PROJECT_NAME}-${VERSION}-src.zip.asc" "https://dist.apache.org/repos/dist/dev/grails/${SVN_FOLDER}/${VERSION}/sources/apache-${PROJECT_NAME}-${VERSION}-src.zip.asc" 47 | curl -f -L -o "${DOWNLOAD_LOCATION}/apache-${PROJECT_NAME}-${VERSION}-src.zip.sha512" "https://dist.apache.org/repos/dist/dev/grails/${SVN_FOLDER}/${VERSION}/sources/apache-${PROJECT_NAME}-${VERSION}-src.zip.sha512" 48 | 49 | echo "Comparing SVN vs GitHub Release files" 50 | set +e 51 | 52 | cmp -s "${DOWNLOAD_LOCATION}/apache-${PROJECT_NAME}-${VERSION}-src.zip.asc" "${DOWNLOAD_LOCATION}/github-apache-${PROJECT_NAME}-${VERSION}-src.zip.asc" 53 | if [ $? -eq 0 ]; then 54 | echo "✅ Identical SVN vs GitHub Upload for apache-${PROJECT_NAME}-${VERSION}-src.zip.asc" 55 | else 56 | echo "❌Different SVN vs GitHub Upload for apache-${PROJECT_NAME}-${VERSION}-src.zip.asc" 57 | exit 1 58 | fi 59 | 60 | set +e 61 | cmp -s "${DOWNLOAD_LOCATION}/apache-${PROJECT_NAME}-${VERSION}-src.zip.sha512" "${DOWNLOAD_LOCATION}/github-apache-${PROJECT_NAME}-${VERSION}-src.zip.sha512" 62 | if [ $? -eq 0 ]; then 63 | echo "✅ Identical SVN vs GitHub Upload for apache-${PROJECT_NAME}-${VERSION}-src.zip.sha512" 64 | else 65 | echo "❌ Different SVN vs GitHub Upload for apache-${PROJECT_NAME}-${VERSION}-src.zip.sha512" 66 | exit 1 67 | fi 68 | 69 | ZIP_SVN_CHECKSUM=$(shasum -a 512 "${DOWNLOAD_LOCATION}/apache-${PROJECT_NAME}-${VERSION}-src.zip" | awk '{print $1}') 70 | ZIP_GITHUB_CHECKSUM=$(shasum -a 512 "${DOWNLOAD_LOCATION}/github-apache-${PROJECT_NAME}-${VERSION}-src.zip" | awk '{print $1}') 71 | if [ "${ZIP_SVN_CHECKSUM}" != "${ZIP_GITHUB_CHECKSUM}" ]; then 72 | echo "❌ Checksum mismatch between SVN and GitHub source zip files" 73 | exit 1 74 | else 75 | echo "✅ Checksum matches between SVN and GitHub source zip files" 76 | fi -------------------------------------------------------------------------------- /gradle/docs-config.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import org.asciidoctor.gradle.jvm.AsciidoctorTask 21 | 22 | apply plugin: 'org.asciidoctor.jvm.convert' 23 | 24 | configurations.register('documentation') { 25 | canBeConsumed = false 26 | canBeResolved = true 27 | attributes { 28 | attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY)) 29 | attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL)) 30 | attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME)) 31 | } 32 | 33 | } 34 | 35 | dependencies { 36 | documentation platform("org.apache.grails:grails-bom:$grailsVersion") 37 | documentation 'org.apache.groovy:groovy-ant' 38 | documentation 'org.apache.groovy:groovy-groovydoc' 39 | } 40 | 41 | tasks.withType(Groovydoc).configureEach { 42 | access = GroovydocAccess.PROTECTED 43 | processScripts = false 44 | includeMainForScripts = false 45 | includeAuthor = false 46 | classpath = configurations.documentation 47 | groovyClasspath = configurations.documentation 48 | } 49 | 50 | tasks.named('asciidoctor', AsciidoctorTask) { 51 | group = 'documentation' 52 | description = 'Generates the reference documentation' 53 | sourceDir = layout.projectDirectory.dir('src/docs') 54 | outputDir = layout.buildDirectory.dir('docs/guide').get().asFile 55 | baseDirFollowsSourceDir() 56 | jvm { 57 | jvmArgs( 58 | '--add-opens', 'java.base/sun.nio.ch=ALL-UNNAMED', 59 | '--add-opens', 'java.base/java.io=ALL-UNNAMED' 60 | ) 61 | } 62 | attributes( 63 | copyright : 'Apache License, Version 2.0', 64 | docinfo1 : 'true', 65 | doctype : 'book', 66 | encoding : 'utf-8', 67 | icons : 'font', 68 | id : name + ':' + version, 69 | idprefix : '', 70 | idseparator : '-', 71 | lang : 'en', 72 | linkattrs : true, 73 | numbered : '', 74 | producer : 'Asciidoctor', 75 | revnumber : version, 76 | setanchors : true, 77 | 'source-highlighter' : 'prettify', 78 | toc : 'left', 79 | toc2 : '', 80 | toclevels : '2' 81 | ) 82 | inputs.dir(sourceDir) 83 | outputs.dir(outputDir) 84 | } 85 | 86 | tasks.register('docs') { 87 | group = 'documentation' 88 | description = 'Generates both API and Guide documentation' 89 | dependsOn('groovydoc', 'asciidoctor') 90 | def redirectFile = layout.buildDirectory.file('docs/index.html') 91 | outputs.file(redirectFile) 92 | doLast { 93 | redirectFile.get().asFile.text = """\ 94 | 95 | 96 | Grails Quartz Plugin Documentation 97 | 98 | 99 | 100 | 101 | 102 | """.stripIndent(12) 103 | } 104 | } -------------------------------------------------------------------------------- /src/test/groovy/grails/plugins/quartz/CustomTriggerFactoryBeanSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package grails.plugins.quartz 21 | 22 | import grails.plugins.quartz.config.TriggersConfigBuilder 23 | 24 | import org.quartz.CronTrigger 25 | import org.quartz.DailyTimeIntervalTrigger 26 | import org.quartz.DateBuilder 27 | import org.quartz.SimpleTrigger 28 | import org.quartz.TimeOfDay 29 | import org.quartz.Trigger 30 | import org.quartz.impl.triggers.DailyTimeIntervalTriggerImpl 31 | import spock.lang.Specification 32 | 33 | /** 34 | * Tests for CustomTriggerFactoryBean 35 | * 36 | * @author Vitalii Samolovskikh aka Kefir 37 | */ 38 | class CustomTriggerFactoryBeanSpec extends Specification { 39 | 40 | private static final String CRON_EXPRESSION = '0 15 6 * * ?' 41 | private static final TimeOfDay START_TIME = new TimeOfDay(10, 0) 42 | private static final TimeOfDay END_TIME = new TimeOfDay(11, 30) 43 | 44 | void 'testFactory'() { 45 | setup: 46 | def builder = new TriggersConfigBuilder('TestJob', null) 47 | def closure = { 48 | simple name: 'simple', group: 'group', startDelay: 500, repeatInterval: 1000, repeatCount: 3 49 | cron name: 'cron', group: 'group', cronExpression: CRON_EXPRESSION 50 | custom name: 'custom', group: 'group', triggerClass: DailyTimeIntervalTriggerImpl, 51 | startTimeOfDay: START_TIME, endTimeOfDay: END_TIME, 52 | repeatIntervalUnit: DateBuilder.IntervalUnit.MINUTE, repeatInterval: 5 53 | } 54 | builder.build(closure) 55 | 56 | Map triggers = [:] 57 | 58 | builder.triggers.values().each { 59 | CustomTriggerFactoryBean factory = new CustomTriggerFactoryBean() 60 | factory.setTriggerClass(it.triggerClass) 61 | factory.setTriggerAttributes(it.triggerAttributes) 62 | factory.afterPropertiesSet() 63 | Trigger trigger = factory.getObject() as Trigger 64 | triggers.put(trigger.key.name, trigger) 65 | } 66 | 67 | expect: 68 | assert triggers['simple'] instanceof SimpleTrigger 69 | SimpleTrigger simpleTrigger = triggers['simple'] as SimpleTrigger 70 | assert 'simple' == simpleTrigger.key.name 71 | assert 'group' == simpleTrigger.key.group 72 | assert 1000 == simpleTrigger.repeatInterval 73 | assert 3 == simpleTrigger.repeatCount 74 | 75 | assert triggers['cron'] instanceof CronTrigger 76 | CronTrigger cronTrigger = triggers['cron'] as CronTrigger 77 | assert 'cron' == cronTrigger.key.name 78 | assert 'group' == cronTrigger.key.group 79 | assert CRON_EXPRESSION == cronTrigger.getCronExpression() 80 | 81 | assert triggers['custom'] instanceof DailyTimeIntervalTrigger 82 | DailyTimeIntervalTrigger customTrigger = triggers['custom'] as DailyTimeIntervalTrigger 83 | assert 'custom' == customTrigger.key.name 84 | assert 'group' == customTrigger.key.group 85 | assert START_TIME == customTrigger.startTimeOfDay 86 | assert END_TIME == customTrigger.endTimeOfDay 87 | assert DateBuilder.IntervalUnit.MINUTE == customTrigger.repeatIntervalUnit 88 | assert 5 == customTrigger.repeatInterval 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /grails-app/services/grails/plugins/quartz/JobManagerService.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grails.plugins.quartz 18 | 19 | import org.quartz.JobKey 20 | import org.quartz.Scheduler 21 | import org.quartz.TriggerKey 22 | import org.quartz.impl.matchers.GroupMatcher 23 | 24 | /** 25 | * JobManagerService simplifies interaction with the Quartz Scheduler from Grails application. 26 | * 27 | * @author Marco Mornati (mmornati@byte-code.com) 28 | * @author Sergey Nebolsin (nebolsin@gmail.com) 29 | * 30 | * @since 0.4 31 | */ 32 | class JobManagerService { 33 | 34 | Scheduler quartzScheduler 35 | 36 | /** 37 | * Returns all the jobs, registered in the Quartz Scheduler, grouped bu their corresponding job groups. 38 | * 39 | * @return Map > with job group names as keys 40 | */ 41 | Map > getAllJobs() { 42 | quartzScheduler.jobGroupNames.collectEntries([:]) { group -> [(group):getJobs(group)]} 43 | } 44 | 45 | /** 46 | * Returns all the jobs, registered in the Quartz Scheduler, which belong to the specified group. 47 | * 48 | * @param group — the jobs group name 49 | * @return a list of corresponding JobDescriptor objects 50 | */ 51 | List getJobs(String group) { 52 | List list = new ArrayList() 53 | quartzScheduler.getJobKeys(GroupMatcher.groupEquals(group)).each { jobKey -> 54 | def jobDetail = quartzScheduler.getJobDetail(jobKey) 55 | if(jobDetail!=null){ 56 | list.add(JobDescriptor.build(jobDetail, quartzScheduler)) 57 | } 58 | } 59 | return list 60 | } 61 | 62 | /** 63 | * Returns a list of all currently executing jobs. 64 | * 65 | * @return a List, containing all currently executing jobs. 66 | */ 67 | def getRunningJobs() { 68 | quartzScheduler.getCurrentlyExecutingJobs() 69 | } 70 | 71 | def pauseJob(String group, String name) { 72 | quartzScheduler.pauseJob(new JobKey(name, group)) 73 | } 74 | 75 | def resumeJob(String group, String name) { 76 | quartzScheduler.resumeJob(new JobKey(name, group)) 77 | } 78 | 79 | def pauseTrigger(String group, String name) { 80 | quartzScheduler.pauseTrigger(new TriggerKey(name, group)) 81 | } 82 | 83 | def resumeTrigger(String group, String name) { 84 | quartzScheduler.resumeTrigger(new TriggerKey(name, group)) 85 | } 86 | 87 | def pauseTriggerGroup(String group) { 88 | quartzScheduler.pauseTriggers(GroupMatcher.groupEquals(group)) 89 | } 90 | 91 | def resumeTriggerGroup(String group) { 92 | quartzScheduler.resumeTriggers(GroupMatcher.groupEquals(group)) 93 | } 94 | 95 | def pauseJobGroup(String group) { 96 | quartzScheduler.pauseJobs(GroupMatcher.groupEquals(group)) 97 | } 98 | 99 | def resumeJobGroup(String group) { 100 | quartzScheduler.resumeJobs(GroupMatcher.groupEquals(group)) 101 | } 102 | 103 | def pauseAll(){ 104 | quartzScheduler.pauseAll() 105 | } 106 | 107 | def resumeAll(){ 108 | quartzScheduler.resumeAll() 109 | } 110 | 111 | def removeJob(String group, String name) { 112 | quartzScheduler.deleteJob(new JobKey(name, group)) 113 | } 114 | 115 | def unscheduleJob(String group, String name) { 116 | quartzScheduler.unscheduleJobs(quartzScheduler.getTriggersOfJob(new JobKey(name, group))*.key) 117 | } 118 | 119 | def interruptJob(String group, String name) { 120 | quartzScheduler.interrupt(new JobKey(name, group)) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugins/quartz/DefaultGrailsJobClass.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grails.plugins.quartz; 18 | 19 | import grails.plugins.quartz.config.TriggersConfigBuilder; 20 | import grails.util.GrailsClassUtils; 21 | import groovy.lang.Closure; 22 | import org.grails.core.AbstractGrailsClass; 23 | import org.quartz.JobExecutionContext; 24 | 25 | import java.util.HashMap; 26 | import java.util.Map; 27 | import static grails.plugins.quartz.GrailsJobClassConstants.*; 28 | 29 | 30 | /** 31 | * Grails artifact class which represents a Quartz job. 32 | * 33 | * @author Micha?? K??ujszo 34 | * @author Marcel Overdijk 35 | * @author Sergey Nebolsin (nebolsin@gmail.com) 36 | * @since 0.1 37 | */ 38 | public class DefaultGrailsJobClass extends AbstractGrailsClass implements GrailsJobClass { 39 | 40 | public static final String JOB = "Job"; 41 | private Map triggers = new HashMap(); 42 | private boolean triggersEvaluated = false; 43 | 44 | 45 | public DefaultGrailsJobClass(Class clazz) { 46 | super(clazz, JOB); 47 | } 48 | 49 | private void evaluateTriggers() { 50 | // registering additional triggersClosure from 'triggersClosure' closure if present 51 | Closure triggersClosure = (Closure) GrailsClassUtils.getStaticPropertyValue(getClazz(), "triggers"); 52 | 53 | TriggersConfigBuilder builder = new TriggersConfigBuilder(getFullName(), grailsApplication); 54 | 55 | if (triggersClosure != null) { 56 | builder.build(triggersClosure); 57 | triggers = (Map) builder.getTriggers(); 58 | } 59 | triggersEvaluated = true; 60 | } 61 | 62 | public void execute() { 63 | getMetaClass().invokeMethod(getReferenceInstance(), EXECUTE, new Object[]{}); 64 | } 65 | 66 | public void execute(JobExecutionContext context) { 67 | getMetaClass().invokeMethod(getReferenceInstance(), EXECUTE, new Object[]{context}); 68 | } 69 | 70 | public String getGroup() { 71 | String group = getStaticPropertyValue(GROUP, String.class); 72 | if (group == null || "".equals(group)) return DEFAULT_GROUP; 73 | return group; 74 | } 75 | 76 | public boolean isConcurrent() { 77 | Boolean concurrent = getStaticPropertyValue(CONCURRENT, Boolean.class); 78 | return concurrent == null ? DEFAULT_CONCURRENT : concurrent; 79 | } 80 | 81 | public boolean isSessionRequired() { 82 | Boolean sessionRequired = getStaticPropertyValue(SESSION_REQUIRED, Boolean.class); 83 | return sessionRequired == null ? DEFAULT_SESSION_REQUIRED : sessionRequired; 84 | } 85 | 86 | public boolean isDurability() { 87 | Boolean durability = getStaticPropertyValue(DURABILITY, Boolean.class); 88 | return durability == null ? DEFAULT_DURABILITY : durability; 89 | } 90 | 91 | public boolean isRequestsRecovery() { 92 | Boolean requestsRecovery = getStaticPropertyValue(REQUESTS_RECOVERY, Boolean.class); 93 | return requestsRecovery == null ? DEFAULT_REQUESTS_RECOVERY : requestsRecovery; 94 | } 95 | 96 | public boolean isEnabled() { 97 | Boolean enabled = getStaticPropertyValue(ENABLED, Boolean.class); 98 | return enabled == null ? DEFAULT_ENABLED : enabled; 99 | } 100 | 101 | public String getDescription() { 102 | String description = (String) getPropertyOrStaticPropertyOrFieldValue(DESCRIPTION, String.class); 103 | if (description == null || "".equals(description)) return DEFAULT_DESCRIPTION; 104 | return description; 105 | } 106 | 107 | public Map getTriggers() { 108 | if (triggersEvaluated == false) { 109 | evaluateTriggers(); 110 | } 111 | return triggers; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugins/quartz/CustomTriggerFactoryBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grails.plugins.quartz; 18 | 19 | import org.quartz.JobDetail; 20 | import org.quartz.Trigger; 21 | import org.quartz.impl.triggers.AbstractTrigger; 22 | import org.springframework.beans.BeanUtils; 23 | import org.springframework.beans.BeanWrapper; 24 | import org.springframework.beans.PropertyAccessorFactory; 25 | import org.springframework.beans.factory.FactoryBean; 26 | import org.springframework.beans.factory.InitializingBean; 27 | 28 | import java.beans.PropertyEditorSupport; 29 | import java.text.ParseException; 30 | import java.util.Date; 31 | import java.util.Map; 32 | 33 | /** 34 | * The factory bean to create and register trigger beans in Spring context. 35 | * 36 | * @author Sergey Nebolsin (nebolsin@gmail.com) 37 | */ 38 | public class CustomTriggerFactoryBean implements FactoryBean, InitializingBean { 39 | private Class triggerClass; 40 | private Trigger customTrigger; 41 | private JobDetail jobDetail; 42 | 43 | private Map triggerAttributes; 44 | 45 | public void afterPropertiesSet() throws ParseException { 46 | // Create a trigger by the class name 47 | customTrigger = BeanUtils.instantiateClass(triggerClass); 48 | 49 | // If trigger is a standard trigger, set standard properties 50 | if(customTrigger instanceof AbstractTrigger){ 51 | AbstractTrigger at =(AbstractTrigger) customTrigger; 52 | 53 | // Set job details 54 | if(jobDetail!=null){ 55 | at.setJobKey(jobDetail.getKey()); 56 | } 57 | 58 | // Set start delay 59 | if (triggerAttributes.containsKey(GrailsJobClassConstants.START_DELAY)) { 60 | Number startDelay = (Number) triggerAttributes.remove(GrailsJobClassConstants.START_DELAY); 61 | at.setStartTime(new Date(System.currentTimeMillis() + startDelay.longValue())); 62 | } else { 63 | at.setStartTime(new Date()); 64 | } 65 | } 66 | 67 | // Set non standard properties. 68 | BeanWrapper customTriggerWrapper = PropertyAccessorFactory.forBeanPropertyAccess(customTrigger); 69 | customTriggerWrapper.registerCustomEditor(String.class, new StringEditor()); 70 | customTriggerWrapper.setPropertyValues(triggerAttributes); 71 | } 72 | 73 | /** 74 | * {@inheritDoc} 75 | * 76 | * @see org.springframework.beans.factory.FactoryBean#getObject() 77 | */ 78 | public Trigger getObject() throws Exception { 79 | return customTrigger; 80 | } 81 | 82 | /** 83 | * {@inheritDoc} 84 | * 85 | * @see org.springframework.beans.factory.FactoryBean#getObjectType() 86 | */ 87 | public Class getObjectType() { 88 | return triggerClass; 89 | } 90 | 91 | /** 92 | * {@inheritDoc} 93 | * 94 | * @see org.springframework.beans.factory.FactoryBean#isSingleton() 95 | */ 96 | public boolean isSingleton() { 97 | return true; 98 | } 99 | 100 | public void setJobDetail(JobDetail jobDetail) { 101 | this.jobDetail = jobDetail; 102 | } 103 | 104 | public void setTriggerClass(Class triggerClass) { 105 | this.triggerClass = triggerClass; 106 | } 107 | 108 | public void setTriggerAttributes(Map triggerAttributes) { 109 | this.triggerAttributes = triggerAttributes; 110 | } 111 | } 112 | 113 | // We need this additional editor to support GString -> String convertion for trigger's properties. 114 | class StringEditor extends PropertyEditorSupport { 115 | @Override 116 | public void setValue(Object value) { 117 | super.setValue(value == null ? null : value.toString()); 118 | } 119 | 120 | @Override 121 | public void setAsText(String text) throws IllegalArgumentException { 122 | setValue(text); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/docs/triggers.adoc: -------------------------------------------------------------------------------- 1 | //// 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | //// 19 | 20 | [[triggers]] 21 | == Understanding Triggers 22 | 23 | === Scheduling configuration syntax 24 | 25 | 26 | Currently, plugin supports three types of triggers: 27 | 28 | * *simple* — executes once per defined interval (ex. “every 10 seconds”); 29 | * *cron* — executes job with cron expression (ex. “at 8:00am every Monday through Friday”); 30 | * *custom* — your implementation of Trigger interface. 31 | 32 | Multiple triggers per job are allowed. 33 | 34 | [source,groovy] 35 | ---- 36 | class MyJob { 37 | 38 | static triggers = { 39 | simple( 40 | name: 'simpleTrigger', 41 | startDelay: 10000, 42 | repeatInterval: 30000, 43 | repeatCount: 10 44 | ) 45 | cron( 46 | name: 'cronTrigger', 47 | startDelay: 10000, 48 | cronExpression: '0/6 * 15 * * ?', 49 | timeZone: TimeZone.getTimeZone('GMT-8') // timeZone is optional 50 | ) 51 | custom( 52 | name: 'customTrigger', 53 | triggerClass: MyTriggerClass, 54 | myParam: myValue, 55 | myAnotherParam: myAnotherValue 56 | ) 57 | } 58 | 59 | void execute() { 60 | println 'Job run!' 61 | } 62 | } 63 | ---- 64 | 65 | With this configuration a job will be executed 11 times with a 30-second interval with the first run in 10 seconds after scheduler startup (simple trigger). It will also be executed each 6 seconds during the 15th hour (15:00:00, 15:00:06, 15:00:12, … — this configured by cron trigger), and also each time your custom trigger will fire. 66 | 67 | Three kinds of triggers are supported with the following parameters: 68 | 69 | * `simple`: 70 | ** `name` — the name that identifies the trigger 71 | ** `startDelay` — delay (in milliseconds) between scheduler startup and first job’s execution 72 | ** `repeatInterval` — timeout (in milliseconds) between consecutive job’s executions 73 | ** `repeatCount` — trigger will fire job execution (1 + repeatCount) times and stop after that (specify zero here to have a one-shot job or -1 to repeat job executions indefinitely) 74 | * `cron`: 75 | ** `name` — the name that identifies the trigger 76 | ** `startDelay` — delay (in milliseconds) between scheduler startup and first job’s execution 77 | ** `cronExpression` — cron expression 78 | * `custom`: 79 | ** `triggerClass` — your class which implements Trigger interface 80 | any params needed by your trigger. 81 | 82 | It is also possible to adjust properties in a trigger Closure by the Grails configuration since the triggers block is given access to the grailsApplication object. 83 | 84 | 85 | ==== Dynamic Job Scheduling 86 | 87 | Starting from the 0.4.1 version, you can schedule job executions dynamically. 88 | 89 | These methods are available: 90 | 91 | [,groovy] 92 | ---- 93 | // creates cron trigger 94 | MyJob.schedule(String cronExpression, Map params) 95 | 96 | // creates simple trigger: repeats job repeatCount+1 times with delay of repeatInterval milliseconds 97 | MyJob.schedule(Long repeatInterval, Integer repeatCount, Map params) 98 | 99 | // schedules one job execution to the specific date 100 | MyJob.schedule(Date scheduleDate, Map params) 101 | 102 | //schedules job's execution with a custom trigger 103 | MyJob.schedule(Trigger trigger) 104 | 105 | // force immediate execution of the job 106 | MyJob.triggerNow(Map params) 107 | 108 | // Each method (except the one for custom trigger) takes optional 'params' argument. 109 | // You can use it to pass some data to your job and then access it from the job. 110 | class MyJob { 111 | void execute(context) { 112 | println(context.mergedJobDataMap.foo) 113 | } 114 | } 115 | 116 | // now in your controller (or service, or something else): 117 | MyJob.triggerNow([foo: 'It Works!']) 118 | ---- 119 | -------------------------------------------------------------------------------- /etc/bin/verify-jar-artifacts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # 20 | 21 | set -euo pipefail 22 | 23 | PROJECT_NAME='grails-quartz' 24 | RELEASE_TAG=$1 25 | DOWNLOAD_LOCATION="${2:-downloads}" 26 | DOWNLOAD_LOCATION=$(realpath "${DOWNLOAD_LOCATION}") 27 | CWD=$(pwd) 28 | 29 | if [ -z "${RELEASE_TAG}" ]; then 30 | echo "Usage: $0 [release-tag] " 31 | exit 1 32 | fi 33 | 34 | VERSION=${RELEASE_TAG#v} 35 | 36 | ARTIFACTS_FILE="${DOWNLOAD_LOCATION}/${PROJECT_NAME}/PUBLISHED_ARTIFACTS" 37 | CHECKSUMS_FILE="${DOWNLOAD_LOCATION}/${PROJECT_NAME}/CHECKSUMS" 38 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 39 | 40 | if [ ! -f "${ARTIFACTS_FILE}" ]; then 41 | echo "Required file ${ARTIFACTS_FILE} not found." 42 | exit 1 43 | fi 44 | 45 | if [ ! -f "${CHECKSUMS_FILE}" ]; then 46 | echo "Required file ${CHECKSUMS_FILE} not found." 47 | exit 1 48 | fi 49 | 50 | export GRAILS_GPG_HOME=$(mktemp -d) 51 | cleanup() { 52 | rm -rf "${GRAILS_GPG_HOME}" 53 | cd "$CWD" 54 | } 55 | trap cleanup EXIT 56 | error() { 57 | echo "❌ JAR Verification failed ❌" 58 | } 59 | trap error ERR 60 | cd "${DOWNLOAD_LOCATION}" 61 | 62 | echo "Importing GPG key to independent GPG home ..." 63 | gpg --homedir "${GRAILS_GPG_HOME}" --import "${DOWNLOAD_LOCATION}/KEYS" 64 | echo "✅ GPG Key Imported" 65 | 66 | REPO_BASE_URL="https://repository.apache.org/content/groups/staging" 67 | 68 | # switch to the extracted project source directory 69 | cd "${PROJECT_NAME}" 70 | 71 | # Create a temporary directory to work in 72 | WORK_DIR='etc/bin/results/first' 73 | mkdir -p "${WORK_DIR}" 74 | echo "Using temp dir: ${WORK_DIR}" 75 | cd "${WORK_DIR}" 76 | 77 | # Read each line from ARTIFACTS_FILE 78 | while IFS= read -r line; do 79 | JAR_FILE=$(echo "${line}" | awk '{print $1}') 80 | [[ "${JAR_FILE}" != *.jar ]] && continue 81 | 82 | COORDINATES=$(echo "${line}" | awk '{print $2}') 83 | 84 | GROUP_ID=$(echo "${COORDINATES}" | cut -d: -f1 | tr '.' '/') 85 | ARTIFACT_ID=$(echo "${COORDINATES}" | cut -d: -f2) 86 | VERSION=$(echo "${COORDINATES}" | cut -d: -f3) 87 | CLASSIFIER=$(echo "${COORDINATES}" | cut -d: -f4-) 88 | 89 | if [[ -n "${CLASSIFIER}" ]]; then 90 | FILE_NAME="${ARTIFACT_ID}-${VERSION}-${CLASSIFIER}.jar" 91 | else 92 | FILE_NAME="${ARTIFACT_ID}-${VERSION}.jar" 93 | fi 94 | 95 | JAR_URL="${REPO_BASE_URL}/${GROUP_ID}/${ARTIFACT_ID}/${VERSION}/${FILE_NAME}" 96 | ASC_URL="${JAR_URL}.asc" 97 | 98 | echo "🔎 Checking artifact: ${FILE_NAME} as ${JAR_FILE}" 99 | if [ ! -f "${JAR_FILE}" ]; then 100 | echo "... Downloading: ${JAR_URL} to ${JAR_FILE}" 101 | curl -sSfL "${JAR_URL}" -o ${JAR_FILE} 102 | else 103 | echo "... Skipping download, already exists: ${JAR_FILE}" 104 | fi 105 | 106 | if [ ! -f "${FILE_NAME}.asc" ]; then 107 | echo "... Downloading signature: ${ASC_URL}" 108 | curl -sSfLO "${ASC_URL}" 109 | else 110 | echo "... Skipping download, already exists: ${FILE_NAME}.asc" 111 | fi 112 | 113 | echo "... Verifying GPG signature..." 114 | gpg --homedir "${GRAILS_GPG_HOME}" --verify "${FILE_NAME}.asc" "${JAR_FILE}" 115 | echo "✅ Verified GPG signature for ${JAR_FILE}" 116 | 117 | EXPECTED_CHECKSUM=$(grep "^${JAR_FILE} " "${CHECKSUMS_FILE}" | awk '{print $2}' || true) 118 | if [ -z "${EXPECTED_CHECKSUM}" ]; then 119 | echo "❌ Checksum not found for ${FILE_NAME}" 120 | exit 1 121 | fi 122 | 123 | echo "... Verifying checksum..." 124 | ACTUAL_CHECKSUM=$(shasum -a 512 "${JAR_FILE}" | awk '{print $1}') 125 | echo "✅ Verified Checksum for ${JAR_FILE}: ${ACTUAL_CHECKSUM}" 126 | 127 | if [ "${ACTUAL_CHECKSUM}" != "${EXPECTED_CHECKSUM}" ]; then 128 | echo "❌ Checksum mismatch for ${JAR_FILE}" 129 | echo "Expected: ${EXPECTED_CHECKSUM}" 130 | echo "Actual: ${ACTUAL_CHECKSUM}" 131 | exit 1 132 | fi 133 | 134 | echo "✅ Verified: ${JAR_FILE}" 135 | done < "${ARTIFACTS_FILE}" 136 | 137 | echo "✅✅✅ All artifacts verified successfully. ✅✅✅" 138 | -------------------------------------------------------------------------------- /src/docs/configuration.adoc: -------------------------------------------------------------------------------- 1 | //// 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | //// 19 | 20 | [[configuration]] 21 | == Plugin Configuration 22 | 23 | 24 | === Configuring the plugin 25 | 26 | The plugin supports configuration settings defined in grails-app/conf/application.yml. 27 | 28 | [source,yml] 29 | ---- 30 | quartz: 31 | autoStartup: true 32 | ---- 33 | 34 | Currently supported options: 35 | 36 | * `autoStartup` Controls automatic startup of the Quartz scheduler during application bootstrap (default: true). 37 | * `jdbcStore` Set to true if you want Quartz to persist jobs in your DB (default: false). You'll also need to provide a `quartz.properties` file and make sure that required tables exist in your db (see the <<_clustering>> section below for the sample config and automatic tables creation using Hibernate). 38 | 39 | 40 | ==== Logging 41 | 42 | A log is auto-injected into your task Job class without having to enable it. To set the logging level, add something like this to your grails-app/conf/Config.groovy log4j configuration. 43 | 44 | [,groovy] 45 | ---- 46 | debug 'grails.app.jobs' 47 | ---- 48 | 49 | 50 | ==== Hibernate Sessions and Jobs 51 | 52 | Jobs are configured by default to have Hibernate Session bounded to thread each time a job is executed. This is required if you are using Hibernate, which requires open session (such as lazy loading of collections) or working with domain objects with unique persistent constraint (it uses Hibernate Session behind the scene). If you want to override this behavior (rarely useful) you can use `sessionRequired` property: 53 | 54 | [,groovy] 55 | ---- 56 | static sessionRequired = false 57 | ---- 58 | 59 | 60 | ==== Configuring concurrent execution 61 | 62 | By default, jobs are executed in concurrent fashion, so new job execution can start even if the previous execution of the same job is still running. If you want to override this behavior, you can use `concurrent` property. In this case Quartz's `StatefulJob` will be used (you can find more info about it here). 63 | 64 | [,groovy] 65 | ---- 66 | static concurrent = false 67 | ---- 68 | 69 | 70 | ==== Configuring Job Enabled 71 | 72 | By default, all jobs are considered enabled. In some cases it may be desired to temporarily disable a job. This can be done by overriding the `jobEnabled` property behavior. 73 | 74 | [,groovy] 75 | ---- 76 | static jobEnabled = false 77 | ---- 78 | 79 | 80 | ==== Configuring description 81 | 82 | Quartz allows for each job to have a short description. This may be configured by adding a description field to your Job. The description can be accessed at runtime using the `JobManagerService` and inspecting the `JobDetail` object. 83 | 84 | [,groovy] 85 | ---- 86 | static description = 'Example Job Description' 87 | ---- 88 | 89 | 90 | [#_clustering] 91 | ==== Clustering 92 | 93 | Quartz plugin doesn't support clustering out-of-the-box now. However, you could use a standard Quartz clustering configuration. You'll also need to set `jdbcStore` configuration option to `true`. 94 | 95 | There are also two parameters for configuring store/clustering on jobs, volatility and durability (both are true by default) and one for triggers, volatility (also true by default). A volatile job and trigger will not persist between Quartz runs, and a durable job will live even when there are no triggers referring to it. 96 | 97 | Read Quartz documentation for more information on clustering and job stores as well as volatility and durability. 98 | 99 | Now that the plugin supports Quartz `2.1.x`, you can use current versions of open source Terracotta see https://github.com/rvanderwerf/terracotta-grails-demo for an example app. 100 | 101 | 102 | ==== Recovering 103 | 104 | Since `0.4.2` recovering from a 'recovery' or 'fail-over' situation is supported with the `requestsRecovery` job-level flag (false by default). 105 | 106 | If a job "requests recovery", and it is executing during a 'hard shutdown' of the scheduler, (i.e., the process it is running within crashes, or the machine is shut off), then it is re-executed when the scheduler is started again. In this case, the `JobExecutionContext.isRecovering()` method will return `true`. 107 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugins/quartz/JobDetailFactoryBeanSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | package grails.plugins.quartz 21 | 22 | import grails.core.GrailsApplication 23 | import org.quartz.JobDetail 24 | import org.quartz.JobKey 25 | import org.springframework.beans.BeanWrapper 26 | import spock.lang.Specification 27 | 28 | /** 29 | * Tests for the JobDetailFactoryBean 30 | * 31 | * @author Vitalii Samolovskikh aka Kefir 32 | */ 33 | class JobDetailFactoryBeanSpec extends Specification { 34 | 35 | private static final String JOB_NAME = 'jobName' 36 | private static final String JOB_GROUP = 'jobGroup' 37 | private static final String JOB_DESCRIPTION = 'The job description' 38 | JobDetailFactoryBean factory = new JobDetailFactoryBean() 39 | 40 | 41 | void 'testFactory1'() { 42 | setup: 43 | factory.jobClass = new GrailsJobClassMock( 44 | [ 45 | fullName : JOB_NAME, 46 | group : JOB_GROUP, 47 | concurrent : true, 48 | durability : true, 49 | sessionRequired : true, 50 | requestsRecovery: true, 51 | description : JOB_DESCRIPTION 52 | ] 53 | ) 54 | factory.afterPropertiesSet() 55 | when: 56 | JobDetail jobDetail = factory.object 57 | then: 58 | new JobKey(JOB_NAME, JOB_GROUP) == jobDetail.key 59 | JOB_NAME == jobDetail.getJobDataMap().get(JobDetailFactoryBean.JOB_NAME_PARAMETER) 60 | jobDetail.durable 61 | !jobDetail.isConcurrentExecutionDisallowed() 62 | !jobDetail.persistJobDataAfterExecution 63 | jobDetail.requestsRecovery() 64 | JOB_DESCRIPTION == jobDetail.description 65 | } 66 | 67 | void 'testFactory2'() { 68 | setup: 69 | factory.jobClass = new GrailsJobClassMock( 70 | [ 71 | fullName : JOB_NAME, 72 | group : JOB_GROUP, 73 | concurrent : false, 74 | durability : false, 75 | sessionRequired : false, 76 | requestsRecovery: false 77 | ] 78 | ) 79 | factory.afterPropertiesSet() 80 | when: 81 | JobDetail jobDetail = factory.object 82 | then: 83 | new JobKey(JOB_NAME, JOB_GROUP) == jobDetail.key 84 | JOB_NAME == jobDetail.getJobDataMap().get(JobDetailFactoryBean.JOB_NAME_PARAMETER) 85 | !jobDetail.durable 86 | jobDetail.isConcurrentExecutionDisallowed() 87 | jobDetail.persistJobDataAfterExecution 88 | !jobDetail.requestsRecovery() 89 | jobDetail.description == null 90 | } 91 | } 92 | 93 | class GrailsJobClassMock implements GrailsJobClass { 94 | String group 95 | String fullName 96 | boolean concurrent 97 | boolean jobEnabled 98 | boolean durability 99 | boolean sessionRequired 100 | boolean requestsRecovery 101 | String description 102 | 103 | void execute() {} 104 | Map getTriggers() {} 105 | boolean byName() { false } 106 | boolean byType() { false } 107 | boolean getAvailable() { false } 108 | boolean isAbstract() { false } 109 | boolean isEnabled() { true } 110 | GrailsApplication getGrailsApplication() {} 111 | 112 | @Override 113 | grails.core.GrailsApplication getApplication() { 114 | return null 115 | } 116 | 117 | Object getPropertyValue(String name) {} 118 | boolean hasProperty(String name) { false } 119 | Object newInstance() {} 120 | String getName() {} 121 | String getShortName() {} 122 | String getPropertyName() {} 123 | String getLogicalPropertyName() {} 124 | String getNaturalName() {} 125 | String getPackageName() {} 126 | Class getClazz() {} 127 | BeanWrapper getReference() {} 128 | Object getReferenceInstance() {} 129 | def T getPropertyValue(String name, Class type) {} 130 | 131 | @Override 132 | String getPluginName() { 133 | return null 134 | } 135 | 136 | void setGrailsApplication(GrailsApplication grailsApplication) {} 137 | } 138 | -------------------------------------------------------------------------------- /.github/scripts/releaseJarFiles.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Licensed to the Apache Software Foundation (ASF) under one 5 | # or more contributor license agreements. See the NOTICE file 6 | # distributed with this work for additional information 7 | # regarding copyright ownership. The ASF licenses this file 8 | # to you under the Apache License, Version 2.0 (the 9 | # "License"); you may not use this file except in compliance 10 | # with the License. You may obtain a copy of the License at 11 | # 12 | # https://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, 15 | # software distributed under the License is distributed on an 16 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | # KIND, either express or implied. See the License for the 18 | # specific language governing permissions and limitations 19 | # under the License. 20 | # 21 | 22 | # ./releaseJarFiles.sh 23 | 24 | set -euo pipefail 25 | 26 | if [[ $# -ne 2 ]]; then 27 | echo "Usage: $0 " >&2 28 | exit 1 29 | fi 30 | 31 | NEXUS_URL="https://repository.apache.org" 32 | STAGING_DESCRIPTION="$1" 33 | NEXUS_USER="$2" 34 | read -r -s -p "Password: " NEXUS_PASS 35 | echo 36 | 37 | if [[ -z "${STAGING_DESCRIPTION}" ]]; then 38 | echo "ERROR: Staging Description must not be empty." >&2 39 | exit 1 40 | fi 41 | if [[ -z "${NEXUS_USER}" ]]; then 42 | echo "ERROR: Username must not be empty." >&2 43 | exit 1 44 | fi 45 | if [[ -z "${NEXUS_PASS}" ]]; then 46 | echo "ERROR: Password must not be empty." >&2 47 | exit 1 48 | fi 49 | 50 | nexusApi() { 51 | local request_method="$1"; shift 52 | local path="$1"; shift 53 | curl -fsS -u "${NEXUS_USER}:${NEXUS_PASS}" \ 54 | -H 'Accept: application/json' \ 55 | -H 'Content-Type: application/json' \ 56 | -X "${request_method}" "${NEXUS_URL}/service/local/${path}" "$@" 57 | } 58 | 59 | wait_for_promotion() { 60 | local repoId="$1" 61 | local timeout_s="${2:-600}" # default 10 minutes 62 | local interval_s="${3:-3}" 63 | local started 64 | started="$(date +%s)" 65 | 66 | echo "Waiting for release promotion to complete (timeout ${timeout_s}s)…" 67 | 68 | while :; do 69 | # 1) If any ERROR appears in activity, fail fast 70 | act="$(nexusApi GET "/staging/repository/${repoId}/activity" || true)" 71 | if [[ -n "$act" ]]; then 72 | err_count="$(jq -r '[.. | objects? | select(has("severity")) | select(.severity=="ERROR")] | length' <<<"$act" 2>/dev/null || echo 0)" 73 | if [[ "$err_count" != "0" ]]; then 74 | echo "ERROR: Staging activity contains failure(s). Aborting." >&2 75 | # Optionally dump recent relevant lines: 76 | jq -r '.. | objects? | select(has("severity")) | "\(.severity): \(.name // "event") - \(.message // "")"' <<<"$act" || true 77 | return 1 78 | fi 79 | fi 80 | 81 | # 2) Check transitioning flag — when false after promote, action is done 82 | trans="$(nexusApi GET '/staging/profile_repositories' \ 83 | | jq -r --arg r "$repoId" '.data[]? | select(.repositoryId==$r) | .transitioning' 2>/dev/null || echo "true")" 84 | 85 | if [[ "$trans" == "false" ]]; then 86 | # sanity: make sure we actually saw some "release/promote" activity; otherwise keep waiting a bit 87 | if [[ -n "$act" ]]; then 88 | # did we see any promote/release-ish step? 89 | saw_promote="$(jq -r ' 90 | [ .. | objects? | .name? // empty | ascii_downcase 91 | | select(test("release|promote|finish")) ] | length' <<<"$act" 2>/dev/null || echo 0)" 92 | if [[ "$saw_promote" -gt 0 ]]; then 93 | echo "Promotion appears complete." 94 | return 0 95 | fi 96 | fi 97 | fi 98 | 99 | # timeout? 100 | now="$(date +%s)" 101 | if (( now - started >= timeout_s )); then 102 | echo "ERROR: Timed out waiting for promotion to complete." >&2 103 | # Show a short summary to aid debugging 104 | if [[ -n "$act" ]]; then 105 | echo "--- Recent activity snapshot ---" 106 | jq -r '.. | objects? | select(has("severity") or has("name")) | "\(.severity // "")\t\(.name // "")\t\(.message // "")"' <<<"$act" | tail -n 20 || true 107 | fi 108 | return 1 109 | fi 110 | 111 | sleep "$interval_s" 112 | done 113 | } 114 | 115 | repos_json="$(nexusApi GET '/staging/profile_repositories')" 116 | repoId="$(jq -r --arg d "${STAGING_DESCRIPTION}" '.data[] | select(.description==$d) | .repositoryId' <<<"${repos_json}")" 117 | profileId="$(jq -r --arg d "${STAGING_DESCRIPTION}" '.data[] | select(.description==$d) | .profileId' <<<"${repos_json}")" 118 | state="$(jq -r --arg d "${STAGING_DESCRIPTION}" '.data[] | select(.description==$d) | .type' <<<"${repos_json}")" 119 | 120 | if [[ -z "${repoId}" || -z "${profileId}" ]]; then 121 | echo "ERROR: No staged repository found with description: ${STAGING_DESCRIPTION}" >&2 122 | exit 2 123 | fi 124 | echo "Found staged repo: ${repoId} (profile: ${profileId}, state: ${state})" 125 | if [[ "${state}" == "open" ]]; then 126 | echo "ERROR: Staged Repo is not closed: ${STAGING_DESCRIPTION}" >&2 127 | exit 3 128 | fi 129 | 130 | if [[ "${state}" == "closed" ]]; then 131 | echo "Promoting (release) ${repoId}…" 132 | nexusApi POST "/staging/profiles/${profileId}/promote" \ 133 | --data "$(jq -n --arg r "${repoId}" --arg d "${STAGING_DESCRIPTION}" '{data:{stagedRepositoryId:$r,description:$d}}')" 134 | fi 135 | 136 | wait_for_promotion "$repoId" 600 3 137 | 138 | echo "Dropping staging repository ${repoId}…" 139 | nexusApi POST "/staging/profiles/${profileId}/drop" \ 140 | --data "$(jq -n --arg r "$repoId" --arg d "${STAGING_DESCRIPTION}" '{data:{stagedRepositoryId:$r,description:$d}}')" 141 | 142 | echo "Done. Released ${repoId}." -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | # Grails Quartz Plugin 18 | 19 | [![Maven Central](https://img.shields.io/maven-central/v/org.apache.grails/grails-quartz.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/org.apache.grails/grails-quartz) 20 | [![Java CI](https://github.com/apache/grails-quartz/actions/workflows/gradle.yml/badge.svg?event=push)](https://github.com/apache/grails-quartz/actions/workflows/gradle.yml) 21 | 22 | ## Documentation 23 | 24 | [Latest documentation](https://apache.github.io/grails-quartz/latest/) and [snapshots](https://apache.github.io/grails-quartz/snapshot/) are available. 25 | 26 | ## Branches 27 | 28 | | Branch | Grails Version | 29 | |--------|----------------| 30 | | 1.x | 2 | 31 | | 2.0.x | 3-5 | 32 | | 3.0.x | 6 | 33 | | 4.0.x | 7 | 34 | 35 | ## Using 36 | ### Quick start 37 | To start using Quartz plugin just simply add 38 | `implementation 'org.apache.grails:grails-quartz:{version}'` in your `build.gradle`. 39 | 40 | >[!NOTE] 41 | > __2.0.13 for Grails 3.3.*__\ 42 | > Properties changed to `static` from `def`.\ 43 | > For example: `def concurrent` will be now `static concurrent`. 44 | 45 | ### Scheduling Jobs 46 | To create a new job run the `grails create-job` command and enter the name of the job. Grails will create a new job and place it in the `grails-app/jobs` directory: 47 | ```groovy 48 | package com.mycompany.myapp 49 | 50 | class MyJob { 51 | 52 | static triggers = { 53 | simple repeatInterval: 1000 54 | } 55 | 56 | void execute() { 57 | println 'Job run!' 58 | } 59 | } 60 | ``` 61 | 62 | The above example will call the `execute()` method every second. 63 | 64 | ### Scheduling configuration syntax 65 | 66 | Currently, the plugin supports three types of [triggers](http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/tutorial-lesson-02.html): 67 | * **simple trigger** — executes once per defined interval (ex. "every 10 seconds"); 68 | * **cron trigger** — executes a job with cron expression (ex. "at 8:00 am every Monday through Friday"); 69 | * **custom trigger** — your implementation of [Trigger](http://www.quartz-scheduler.org/api/2.3.0/org/quartz/Trigger.html) interface. 70 | 71 | Multiple triggers per job are allowed. 72 | ```groovy 73 | class MyJob { 74 | 75 | static triggers = { 76 | simple name: 'simpleTrigger', startDelay: 10000, repeatInterval: 30000, repeatCount: 10 77 | cron name: 'cronTrigger', startDelay: 10000, cronExpression: '0/6 * 15 * * ?' 78 | custom name: 'customTrigger', triggerClass: MyTriggerClass, myParam: myValue, myAnotherParam: myAnotherValue 79 | } 80 | 81 | void execute() { 82 | println 'Job run!' 83 | } 84 | } 85 | ``` 86 | 87 | With this configuration, job will be executed 11 times with 30 seconds interval with first run in 10 seconds after scheduler startup (simple trigger), also it'll be executed each 6 second during 15th hour (15:00:00, 15:00:06, 15:00:12, ... — this configured by cron trigger) and also it'll be executed each time your custom trigger will fire. 88 | 89 | Three kinds of triggers are supported with the following parameters. The name field must be unique: 90 | * `simple`: 91 | * `name` — the name that identifies the trigger; 92 | * `startDelay` — delay (in milliseconds) between scheduler startup and first job's execution; 93 | * `repeatInterval` — timeout (in milliseconds) between consecutive job's executions; 94 | * `repeatCount` — trigger will fire job execution `(1 + repeatCount)` times and stop after that (specify `0` here to have one-shot job or `-1` to repeat job executions indefinitely); 95 | * `cron`: 96 | * `name` — the name that identifies the trigger; 97 | * `startDelay` — delay (in milliseconds) between scheduler startup and first job's execution; 98 | * `cronExpression` — [cron expression](http://www.quartz-scheduler.org/api/2.3.0/org/quartz/CronExpression.html) 99 | * `custom`: 100 | * `triggerClass` — your class which implements [CalendarIntervalTriggerImpl](http://www.quartz-scheduler.org/api/2.3.0/org/quartz/impl/triggers/CalendarIntervalTriggerImpl.html) impl; 101 | * any params needed by your trigger. 102 | 103 | ### Configuration plugin syntax 104 | 105 | You can add the following properties to control persistence or not persistence: 106 | * `quartz.pluginEnabled` - defaults to `true`, can disable plugin for test cases etc. 107 | * `quartz.jdbcStore` - `true` to enable database store, `false` to use RamStore (default: `true`) 108 | * `quartz.autoStartup` - delays jobs until after bootstrap startup phase (default: `false`) 109 | * `quartz.jdbcStoreDataSource` - jdbc data source alternate name 110 | * `quartz.waitForJobsToCompleteOnShutdown` - wait for jobs to complete on shutdown (default: `true`) 111 | * `quartz.exposeSchedulerInRepository` - expose Schedule in repository 112 | * `quartz.scheduler.instanceName` - name of the scheduler to avoid conflicts between apps 113 | * `quartz.purgeQuartzTablesOnStartup` - when jdbcStore set to `true` and this is `true`, clears out all quartz tables on startup 114 | 115 | ## Building from Source 116 | 117 | To build this project from source, you'll need Gradle installed.\ 118 | First, to bootstrap Gradle Wrapper with the correct version in the project directory, run the following commands: 119 | ```console 120 | cd gradle-bootstrap 121 | gradle 122 | cd - 123 | ``` 124 | 125 | After bootstrapping Gradle Wrapper, you can build and run the tests with the command: 126 | ```console 127 | ./gradlew build 128 | ``` 129 | 130 | To run only run the build and skip the tests, run: 131 | ```console 132 | ./gradlew build -PskipTests 133 | ``` 134 | 135 | Then publish the jar files to `mavenLocal` for usage: 136 | ```console 137 | ./gradlew publishToMavenLocal 138 | ``` 139 | -------------------------------------------------------------------------------- /.github/workflows/release-abort.yml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | name: "Release - Abort Release" 17 | on: 18 | workflow_dispatch: 19 | inputs: 20 | release_tag: 21 | description: 'Release tag (e.g., v7.0.0-M5)' 22 | required: true 23 | type: string 24 | permissions: 25 | contents: write 26 | actions: write 27 | jobs: 28 | abort: 29 | name: "Abort Release" 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: "Output Agent IP" # in the event RAO blocks this agent, this can be used to debug it 33 | run: curl -s https://api.ipify.org 34 | - name: "Setup SVN and Tools" 35 | run: sudo apt-get install -y subversion subversion-tools tree 36 | - name: "Extract repository name" 37 | id: extract_repository_name 38 | run: | 39 | echo "repository_name=${GITHUB_REPOSITORY##*/}" >> $GITHUB_OUTPUT 40 | - name: "Extract release version" 41 | id: release_version 42 | run: | 43 | version="${{ github.event.inputs.release_tag }}" 44 | version="${version#v}" 45 | echo "Extracted version: $version" 46 | echo "value=${version}" >> $GITHUB_OUTPUT 47 | - name: "Drop staging repository from Nexus" 48 | continue-on-error: true 49 | env: 50 | NEXUS_STAGE_DEPLOYER_USER: ${{ secrets.NEXUS_STAGE_DEPLOYER_USER }} 51 | NEXUS_STAGE_DEPLOYER_PW: ${{ secrets.NEXUS_STAGE_DEPLOYER_PW }} 52 | run: | 53 | export REPO_DESCRIPTION="${{ steps.extract_repository_name.outputs.repository_name }}:${{ steps.release_version.outputs.value }}" 54 | export STAGING_REPOSITORY_ID=$(curl -s -u "$NEXUS_STAGE_DEPLOYER_USER:$NEXUS_STAGE_DEPLOYER_PW" -H "Accept: application/json" \ 55 | "https://repository.apache.org/service/local/staging/profile_repositories/${{ secrets.STAGING_PROFILE_ID }}" | 56 | jq -r '.data[] | select(.description=="'"$REPO_DESCRIPTION"'") | .repositoryId') 57 | 58 | test -n "$STAGING_REPOSITORY_ID" || { echo "No repo with that description"; exit 1; } 59 | 60 | response=$(curl -s --request POST -u "$NEXUS_STAGE_DEPLOYER_USER:$NEXUS_STAGE_DEPLOYER_PW" \ 61 | --url https://repository.apache.org/service/local/staging/bulk/drop \ 62 | --header 'Content-Type: application/json' \ 63 | --header 'Accept: application/json' \ 64 | --header 'User-Agent: Grails Github Actions' \ 65 | --data '{ "data" : {"stagedRepositoryIds":["'"$STAGING_REPOSITORY_ID"'"], "description":"Drop '"$STAGING_REPOSITORY_ID"'." } }') 66 | 67 | if [ ! -z "$response" ]; then 68 | echo "Error while dropping staged repository $STAGING_REPOSITORY_ID : $response." 69 | exit 1 70 | else 71 | echo "Successfully dropped repository $STAGING_REPOSITORY_ID." 72 | fi 73 | - name: "Remove Staged Artifacts" 74 | continue-on-error: true 75 | env: 76 | SVN_USERNAME: ${{ secrets.SVC_DIST_GRAILS_USERNAME }} 77 | SVN_PASSWORD: ${{ secrets.SVC_DIST_GRAILS_PASSWORD }} 78 | run: | 79 | export VERSION="${{ steps.release_version.outputs.value }}" 80 | svnmucc --username "$SVN_USERNAME" --password "$SVN_PASSWORD" --non-interactive \ 81 | -m "Remove grails dev version $VERSION" \ 82 | rm "https://dist.apache.org/repos/dist/dev/grails/quartz/$VERSION" 83 | - name: "Cancel GitHub Actions" 84 | continue-on-error: true 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | OWNER: ${{ github.repository_owner }} 88 | REPO: ${{ steps.extract_repository_name.outputs.repository_name }} 89 | run: | 90 | for status in queued in_progress; do 91 | curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \ 92 | -H "Accept: application/vnd.github+json" \ 93 | "https://api.github.com/repos/$OWNER/$REPO/actions/runs?event=release&status=$status&per_page=100" | 94 | jq -r '.workflow_runs[].id' 95 | done > run-ids.txt 96 | 97 | while read run_id; do 98 | echo "cancelling $run_id" 99 | curl -s -X POST \ 100 | -H "Authorization: Bearer $GITHUB_TOKEN" \ 101 | -H "Accept: application/vnd.github+json" \ 102 | "https://api.github.com/repos/$OWNER/$REPO/actions/runs/$run_id/cancel" 103 | done < run-ids.txt 104 | rm -f run-ids.txt || true 105 | - name: "Remove GitHub Release & Tag" 106 | env: 107 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 108 | TAG: ${{ github.event.inputs.release_tag }} 109 | OWNER: ${{ github.repository_owner }} 110 | REPO: ${{ steps.extract_repository_name.outputs.repository_name }} 111 | run: | 112 | set -euo pipefail 113 | 114 | if release_json="$(gh api -H 'Accept: application/vnd.github+json' \ 115 | "/repos/$OWNER/$REPO/releases/tags/$TAG" 2>/dev/null)"; then 116 | if [ "$(jq -r '.prerelease' <<<"$release_json")" != "true" ]; then 117 | echo "❌ Release $TAG exists but is *not* marked as a pre-release. Aborting." 118 | exit 1 119 | fi 120 | 121 | release_id="$(jq -r '.id' <<<"$release_json")" 122 | echo "Deleting pre-release $release_id linked to tag $TAG" 123 | gh api -X DELETE "/repos/$OWNER/$REPO/releases/$release_id" 124 | else 125 | echo "No GitHub release found for tag $TAG – skipping release deletion" 126 | fi 127 | 128 | ref="tags/$TAG" 129 | echo "Deleting git ref $ref" 130 | gh api -X DELETE "/repos/$OWNER/$REPO/git/refs/$ref" || echo "Tag $TAG already absent" -------------------------------------------------------------------------------- /src/main/groovy/grails/plugins/quartz/GrailsJobFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grails.plugins.quartz; 18 | 19 | import org.quartz.DisallowConcurrentExecution; 20 | import org.quartz.InterruptableJob; 21 | import org.quartz.JobExecutionContext; 22 | import org.quartz.JobExecutionException; 23 | import org.quartz.PersistJobDataAfterExecution; 24 | import org.quartz.UnableToInterruptJobException; 25 | import org.quartz.spi.TriggerFiredBundle; 26 | import org.springframework.beans.BeansException; 27 | import org.springframework.context.ApplicationContext; 28 | import org.springframework.context.ApplicationContextAware; 29 | import org.springframework.scheduling.quartz.AdaptableJobFactory; 30 | import org.springframework.util.ReflectionUtils; 31 | 32 | import java.lang.reflect.InvocationTargetException; 33 | import java.lang.reflect.Method; 34 | import java.text.MessageFormat; 35 | 36 | /** 37 | * Job factory which retrieves Job instances from ApplicationContext. 38 | *

39 | * It is used by the quartz scheduler to create an instance of the job class for executing. 40 | *

41 | * @author Sergey Nebolsin (nebolsin@gmail.com) 42 | * @since 0.3.2 43 | */ 44 | public class GrailsJobFactory extends AdaptableJobFactory implements ApplicationContextAware { 45 | private ApplicationContext applicationContext; 46 | 47 | @Override 48 | protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { 49 | String grailsJobName = (String) bundle.getJobDetail().getJobDataMap().get( 50 | JobDetailFactoryBean.JOB_NAME_PARAMETER 51 | ); 52 | if (grailsJobName != null) { 53 | return new GrailsJob(applicationContext.getBean(grailsJobName)); 54 | } else { 55 | return super.createJobInstance(bundle); 56 | } 57 | } 58 | 59 | /** 60 | * Quartz Job implementation that invokes execute() on the application's job class. 61 | */ 62 | public static class GrailsJob implements InterruptableJob { 63 | private Object job; 64 | private Method executeMethod; 65 | private Method interruptMethod; 66 | boolean passExecutionContext; 67 | 68 | public GrailsJob(Object job) { 69 | this.job = job; 70 | 71 | // Finds an execute method with zero or one parameter. 72 | this.executeMethod = ReflectionUtils.findMethod( 73 | job.getClass(), GrailsJobClassConstants.EXECUTE, (Class[]) null 74 | ); 75 | if (executeMethod == null) { 76 | throw new IllegalArgumentException( 77 | MessageFormat.format( 78 | "{0} should declare #{1}() method", 79 | job.getClass().getName(), GrailsJobClassConstants.EXECUTE 80 | ) 81 | ); 82 | } 83 | switch (executeMethod.getParameterTypes().length) { 84 | case 0: 85 | passExecutionContext = false; 86 | break; 87 | case 1: 88 | passExecutionContext = true; 89 | break; 90 | default: 91 | throw new IllegalArgumentException( 92 | MessageFormat.format( 93 | "{0}#{1}() method should take either no arguments or one argument of type JobExecutionContext", 94 | job.getClass().getName(), GrailsJobClassConstants.EXECUTE 95 | ) 96 | ); 97 | } 98 | 99 | // Find interrupt method 100 | this.interruptMethod = ReflectionUtils.findMethod(job.getClass(), GrailsJobClassConstants.INTERRUPT); 101 | } 102 | 103 | // Execute Job 104 | public void execute(final JobExecutionContext context) throws JobExecutionException { 105 | try { 106 | if (passExecutionContext) { 107 | executeMethod.invoke(job, context); 108 | } else { 109 | executeMethod.invoke(job); 110 | } 111 | } catch (InvocationTargetException ite) { 112 | Throwable targetException = ite.getTargetException(); 113 | if (targetException instanceof JobExecutionException) { 114 | throw (JobExecutionException) targetException; 115 | } else { 116 | throw new JobExecutionException(targetException); 117 | } 118 | } catch (IllegalAccessException iae) { 119 | JobExecutionException criticalError = new JobExecutionException( 120 | MessageFormat.format( 121 | "Cannot invoke {0}#{1}() method", 122 | job.getClass().getName(), executeMethod.getName() 123 | ), 124 | iae 125 | ); 126 | criticalError.setUnscheduleAllTriggers(true); 127 | throw criticalError; 128 | } 129 | } 130 | 131 | // Interrupt Job 132 | public void interrupt() throws UnableToInterruptJobException { 133 | if (interruptMethod != null) { 134 | try { 135 | interruptMethod.invoke(job); 136 | } catch (Throwable e) { 137 | throw new UnableToInterruptJobException(e); 138 | } 139 | } else { 140 | throw new UnableToInterruptJobException(job.getClass().getName() + " doesn't support interruption"); 141 | } 142 | } 143 | 144 | /** 145 | * It's needed for the quartz-monitor plugin. 146 | * 147 | * @return the GrailsJobClass object. 148 | */ 149 | @SuppressWarnings("UnusedDeclaration") 150 | public Object getJob() { 151 | return job; 152 | } 153 | } 154 | 155 | /** 156 | * Extension of the GrailsJob, has concurrent annotations. 157 | * Quartz checks whether or not jobs are stateful and if so, 158 | * won't let jobs interfere with each other. 159 | */ 160 | @PersistJobDataAfterExecution 161 | @DisallowConcurrentExecution 162 | public static class StatefulGrailsJob extends GrailsJob { 163 | public StatefulGrailsJob(Object job) { 164 | super(job); 165 | } 166 | } 167 | 168 | /** 169 | * Override from ApplicationContextAware. 170 | */ 171 | @Override 172 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 173 | this.applicationContext = applicationContext; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /etc/bin/verify-reproducible.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | # This file assumes the gnu version of coreutils is installed, which is not installed by default on a mac 20 | set -e 21 | 22 | PROJECT_NAME='grails-quartz' 23 | DOWNLOAD_LOCATION="${1:-downloads}" 24 | DOWNLOAD_LOCATION=$(realpath "${DOWNLOAD_LOCATION}") 25 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 26 | 27 | CWD=$(pwd) 28 | 29 | cleanup() { 30 | echo "❌ Verification failed. ❌" 31 | } 32 | trap cleanup ERR 33 | 34 | cd "${DOWNLOAD_LOCATION}/${PROJECT_NAME}" 35 | echo "Searching under ${DOWNLOAD_LOCATION}" 36 | 37 | mkdir -p "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results" 38 | if [[ -f "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/CHECKSUMS" ]]; then 39 | echo "✅ File 'CHECKSUMS' exists." 40 | else 41 | echo "❌ File 'CHECKSUMS' not found. ${PROJECT_NAME} Source Distributions should have a CHECKSUMS file at the root..." 42 | exit 1 43 | fi 44 | 45 | if [[ -f "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/BUILD_DATE" ]]; then 46 | echo "✅ File 'BUILD_DATE' exists." 47 | else 48 | echo "❌ File 'BUILD_DATE' not found. ${PROJECT_NAME} Source Distributions should have a BUILD_DATE file at the root..." 49 | exit 1 50 | fi 51 | export SOURCE_DATE_EPOCH=$(cat "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/BUILD_DATE") 52 | export TEST_BUILD_REPRODUCIBLE='true' 53 | 54 | if [[ -d "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results/first" ]]; then 55 | echo "✅ Directory containing downloaded jar files exists ('first')." 56 | else 57 | echo "❌ Directory 'first' not found. Please place the published jar files under ${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results/first..." 58 | exit 1 59 | fi 60 | 61 | killall -e java || true 62 | ./gradlew publishToMavenLocal --rerun-tasks -PskipTests --no-build-cache 63 | echo "Generating Checksums for Built Jars" 64 | "${SCRIPT_DIR}/generate-build-artifact-hashes.groovy" "${DOWNLOAD_LOCATION}/${PROJECT_NAME}" > "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results/second.txt" 65 | if [ -e "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results/second.txt" ] && [ ! -s "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results/second.txt" ]; then 66 | echo "❌ Error: Could not find any checksums for built jar files!" 67 | exit 1 68 | fi 69 | 70 | echo "Flattening Checksum file" 71 | ## Flatten the jar files since our published artifacts are flat 72 | tmpfile=$(mktemp) 73 | while read -r filepath checksum; do 74 | printf '%s %s\n' "$(basename "$filepath")" "$checksum" 75 | done < "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results/second.txt" > "$tmpfile" && mv "$tmpfile" "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results/second.txt" 76 | 77 | echo "Filtering non-published jars" 78 | # filter to only published jars to compare against 79 | cut -d' ' -f1 "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/CHECKSUMS" | grep -Ff - "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results/second.txt" > "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results/filtered.txt" 80 | rm -f "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results/second.txt" 81 | mv "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results/filtered.txt" "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results/second.txt" 82 | 83 | mkdir -p "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results/second" 84 | find . -path ./etc -prune -o -type f -path '*/build/libs/*.jar' ! -name "buildSrc.jar" -exec cp -t "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results/second/" -- {} + 85 | 86 | cd "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results" 87 | 88 | echo "Checking for differences in checksums" 89 | # diff -u CHECKSUMS second.txt 90 | DIFF_RESULTS=$(comm -3 <(sort ../../../CHECKSUMS) <(sort second.txt) | cut -d' ' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v '^$' | uniq | sort) 91 | echo "${DIFF_RESULTS}" > diff.txt 92 | 93 | if [ -n "${DIFF_RESULTS}" ]; then 94 | echo "${DIFF_RESULTS}" > diff.txt 95 | else 96 | > diff.txt # Empty the file explicitly 97 | fi 98 | 99 | if [ -s diff.txt ]; then 100 | echo "Differences were found, diffing jar files ..." 101 | if [[ ! -f "vineflower.jar" ]]; then 102 | echo "Downloading Vineflower decompiler..." 103 | curl -sL -o "vineflower.jar" https://github.com/Vineflower/vineflower/releases/download/1.11.1/vineflower-1.11.1.jar 104 | if [[ $? -ne 0 ]]; then 105 | echo "❌ Failed to download vineflower.jar ❌" 106 | exit 1 107 | fi 108 | fi 109 | 110 | while IFS= read -r jar_file; do 111 | echo "Checking jar '${jar_file}'..." 112 | 113 | echo "Extracting ${jar_file}" 114 | "${SCRIPT_DIR}/extract-build-artifact.sh" "${jar_file}" "${DOWNLOAD_LOCATION}/${PROJECT_NAME}/etc/bin/results" 115 | echo "✅ Extracted ${jar_file} to firstArtifact and secondArtifact directories." 116 | 117 | # Check extraction success 118 | if [[ ! -d "firstArtifact" || ! -d "secondArtifact" ]]; then 119 | echo "❌ Missing extracted artifacts for ${jar_file} ❌" 120 | echo "${jar_file}" >> diff_purged.txt 121 | continue 122 | fi 123 | 124 | rm -rf "firstSource" "secondSource" || true 125 | mkdir -p "firstSource" "secondSource" 126 | 127 | echo "Decompiling ${jar_file} class files..." 128 | java -jar vineflower.jar firstArtifact firstSource > /dev/null 2>&1 129 | java -jar vineflower.jar secondArtifact secondSource > /dev/null 2>&1 130 | echo "✅ Decompiled ${jar_file}" 131 | 132 | set +e 133 | DIFF_RESULT=$(diff -r -q "firstSource" "secondSource") 134 | set -e 135 | 136 | if [[ -z "${DIFF_RESULT}" ]]; then 137 | echo "✅ No differences remain for ${jar_file}. Removing from diff.txt." 138 | else 139 | echo "❌ Differences still found in ${jar_file}." 140 | echo "${jar_file}" >> diff_purged.txt 141 | fi 142 | 143 | done < diff.txt 144 | : > diff_purged.txt # Ensure the file exists and is empty 145 | mv diff_purged.txt diff.txt 146 | rm -rf firstArtifact secondArtifact firstSource secondSource || true 147 | 148 | if [ -s diff.txt ]; then 149 | echo "❌ Differences Found ❌" 150 | cat diff.txt 151 | echo "❌ Differences Found ❌" 152 | else 153 | echo "✅ Differences were resolved via decompilation. ✅" 154 | exit 0 155 | fi 156 | else 157 | echo "✅ No Differences Found. ✅" 158 | exit 0 159 | fi 160 | 161 | printf '%s\n' "${DIFF_RESULTS}" | sed 's|^etc/bin/results/||' > toPurge.txt 162 | find first -type f -name '*.jar' -print | sed 's|^first/||' | grep -F -x -v -f toPurge.txt | 163 | while IFS= read -r f; do 164 | rm -f "./first/$f" 165 | done 166 | find second -type f -name '*.jar' -print | sed 's|^second/||' | grep -F -x -v -f toPurge.txt | 167 | while IFS= read -r f; do 168 | rm -f "./second/$f" 169 | done 170 | rm toPurge.txt 171 | find . -type d -empty -delete 172 | cd "$CWD" 173 | exit 1 -------------------------------------------------------------------------------- /src/test/groovy/grails/plugins/quartz/config/TriggersConfigBuilderSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grails.plugins.quartz.config 18 | 19 | import grails.plugins.quartz.CustomTriggerFactoryBean 20 | import grails.plugins.quartz.GrailsJobClassConstants as Constants 21 | import spock.lang.Specification 22 | 23 | /** 24 | * Create 10 triggers with different attributes. 25 | * 26 | * @author Sergey Nebolsin (nebolsin@gmail.com) 27 | */ 28 | class TriggersConfigBuilderSpec extends Specification { 29 | 30 | 31 | void 'testConfigBuilder'() { 32 | setup: 33 | def builder = new TriggersConfigBuilder('TestJob', null) 34 | def closure = { 35 | simple() 36 | simple timeout: 1000 37 | simple startDelay: 500 38 | simple startDelay: 500, timeout: 1000 39 | simple startDelay: 500, timeout: 1000, repeatCount: 3 40 | simple name: 'everySecond', timeout: 1000 41 | cron() 42 | cron cronExpression: '0 15 6 * * ?' 43 | cron name: 'myTrigger', cronExpression: '0 15 6 * * ?' 44 | simple startDelay: 500, timeout: 1000, repeatCount: 0 45 | } 46 | builder.build(closure) 47 | expect: 48 | assert 10 == builder.triggers.size(): 'Invalid triggers count' 49 | when: 'TestJob0' 50 | def triggerName = 'TestJob0' 51 | then: 'Verify attributes for TestJob0' 52 | assert builder.triggers[triggerName]?.clazz == CustomTriggerFactoryBean 53 | assertPropertiesEquals(new Expando( 54 | name: triggerName, 55 | group: Constants.DEFAULT_TRIGGERS_GROUP, 56 | startDelay: Constants.DEFAULT_START_DELAY, 57 | repeatInterval: Constants.DEFAULT_REPEAT_INTERVAL, 58 | repeatCount: Constants.DEFAULT_REPEAT_COUNT, 59 | ), builder.triggers[triggerName].triggerAttributes) 60 | when: 'TestJob1' 61 | triggerName = 'TestJob1' 62 | then: 'Verify attributes for TestJob1' 63 | assert builder.triggers[triggerName]?.clazz == CustomTriggerFactoryBean 64 | assertPropertiesEquals(new Expando( 65 | name: triggerName, 66 | group: Constants.DEFAULT_TRIGGERS_GROUP, 67 | startDelay: Constants.DEFAULT_START_DELAY, 68 | repeatInterval: 1000, 69 | repeatCount: Constants.DEFAULT_REPEAT_COUNT, 70 | ), builder.triggers[triggerName].triggerAttributes) 71 | when: 'TestJob2' 72 | triggerName = 'TestJob2' 73 | then: 'Verify attributes for TestJob2' 74 | assert builder.triggers[triggerName]?.clazz == CustomTriggerFactoryBean 75 | assertPropertiesEquals(new Expando( 76 | name: triggerName, 77 | group: Constants.DEFAULT_TRIGGERS_GROUP, 78 | startDelay: 500, 79 | repeatInterval: Constants.DEFAULT_REPEAT_INTERVAL, 80 | repeatCount: Constants.DEFAULT_REPEAT_COUNT, 81 | ), builder.triggers[triggerName].triggerAttributes) 82 | when: 'TestJob3' 83 | triggerName = 'TestJob3' 84 | then: 'Verify attributes for TestJob3' 85 | assert builder.triggers[triggerName]?.clazz == CustomTriggerFactoryBean 86 | assertPropertiesEquals(new Expando( 87 | name: triggerName, 88 | group: Constants.DEFAULT_TRIGGERS_GROUP, 89 | startDelay: 500, 90 | repeatInterval: 1000, 91 | repeatCount: Constants.DEFAULT_REPEAT_COUNT, 92 | ), builder.triggers[triggerName].triggerAttributes) 93 | when: 'TestJob4' 94 | triggerName = 'TestJob4' 95 | then: 'Verify attributes for TestJob4' 96 | assert builder.triggers[triggerName]?.clazz == CustomTriggerFactoryBean 97 | assertPropertiesEquals(new Expando( 98 | name: triggerName, 99 | group: Constants.DEFAULT_TRIGGERS_GROUP, 100 | startDelay: 500, 101 | repeatInterval: 1000, 102 | repeatCount: 3, 103 | ), builder.triggers[triggerName].triggerAttributes) 104 | when: 'everySecond' 105 | triggerName = 'everySecond' 106 | then: 'Verify attribute everySecond' 107 | assert builder.triggers[triggerName]?.clazz == CustomTriggerFactoryBean 108 | assertPropertiesEquals(new Expando( 109 | name: triggerName, 110 | group: Constants.DEFAULT_TRIGGERS_GROUP, 111 | startDelay: Constants.DEFAULT_START_DELAY, 112 | repeatInterval: 1000, 113 | repeatCount: Constants.DEFAULT_REPEAT_COUNT, 114 | ), builder.triggers[triggerName].triggerAttributes 115 | ) 116 | when: 'TestJob5' 117 | triggerName = 'TestJob5' 118 | then: 'Verify attributes TestJob5' 119 | assert builder.triggers[triggerName]?.clazz == CustomTriggerFactoryBean 120 | assertPropertiesEquals(new Expando( 121 | name: triggerName, 122 | group: Constants.DEFAULT_TRIGGERS_GROUP, 123 | startDelay: Constants.DEFAULT_START_DELAY, 124 | cronExpression: Constants.DEFAULT_CRON_EXPRESSION, 125 | ), builder.triggers[triggerName].triggerAttributes 126 | ) 127 | when: 'TestJob6' 128 | triggerName = 'TestJob6' 129 | then: 'Verify attributes TestJob6' 130 | assert builder.triggers[triggerName]?.clazz == CustomTriggerFactoryBean 131 | assertPropertiesEquals(new Expando( 132 | name: triggerName, 133 | group: Constants.DEFAULT_TRIGGERS_GROUP, 134 | cronExpression: '0 15 6 * * ?', 135 | startDelay: Constants.DEFAULT_START_DELAY, 136 | ), builder.triggers[triggerName].triggerAttributes) 137 | when: 'myTrigger' 138 | triggerName = 'myTrigger' 139 | then: 'Verify attribute myTrigger' 140 | assert builder.triggers[triggerName]?.clazz == CustomTriggerFactoryBean 141 | assertPropertiesEquals(new Expando( 142 | name: triggerName, 143 | group: Constants.DEFAULT_TRIGGERS_GROUP, 144 | startDelay: Constants.DEFAULT_START_DELAY, 145 | cronExpression: '0 15 6 * * ?', 146 | ), builder.triggers[triggerName].triggerAttributes) 147 | when: 'TestJob7' 148 | triggerName = 'TestJob7' 149 | then: 'Verify attribute TestJob7' 150 | assert builder.triggers[triggerName]?.clazz == CustomTriggerFactoryBean 151 | assertPropertiesEquals(new Expando( 152 | name: triggerName, 153 | group: Constants.DEFAULT_TRIGGERS_GROUP, 154 | startDelay: 500, 155 | repeatInterval: 1000, 156 | repeatCount: 0, 157 | ), builder.triggers[triggerName].triggerAttributes) 158 | } 159 | 160 | private static void assertPropertiesEquals(expected, actual) { 161 | expected.properties.each { entry -> 162 | assert actual[entry.key] == entry.value, "Unexpected value for property: ${entry.key}" 163 | } 164 | assert actual.size() == expected.properties?.size(), 'Different number of properties' 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /gradle/publish-config.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * https://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import org.gradle.crypto.checksum.Checksum 21 | 22 | apply plugin: 'org.apache.grails.gradle.grails-publish' 23 | 24 | grailsPublish { 25 | githubSlug = 'apache/grails-quartz' 26 | license.name = 'Apache-2.0' 27 | title = 'Grails Quartz Plugin' 28 | desc = 'This plugin allows your Grails application to schedule jobs to be executed using a specified interval or cron expression.' 29 | organization { 30 | name = 'Apache Software Foundation' 31 | url = 'https://apache.org/' 32 | } 33 | developers = [ 34 | burtbeckwith: 'Burt Beckwith', 35 | jeffscottbrown: 'Jeff Scott Brown', 36 | graemerocher: 'Graeme Rocher', 37 | ryanvanderwerf: 'Ryan Vanderwerf', 38 | sergeynebolsin: 'Sergey Nebolsin', 39 | puneetbehl: 'Puneet Behl', 40 | vitaliisamolovskikh: 'Vitalii Samolovskikh' 41 | ] 42 | } 43 | 44 | afterEvaluate { 45 | if (plugins.hasPlugin('signing') && System.getenv('TEST_BUILD_REPRODUCIBLE')) { 46 | logger.lifecycle('Signing is disabled for this build to test build reproducibility.') 47 | tasks.withType(Sign).configureEach { 48 | enabled = false 49 | } 50 | } 51 | if (plugins.hasPlugin('maven-publish')) { 52 | def checksumTask = tasks.register('publishedChecksums', Checksum) { 53 | checksumAlgorithm = Checksum.Algorithm.SHA512 54 | outputDirectory = layout.buildDirectory.dir('checksums') 55 | dependsOn(tasks.withType(Jar)) 56 | } 57 | def artifactsDir = layout.buildDirectory.dir('artifacts') 58 | def artifactsTask = tasks.register('savePublishedArtifacts') { 59 | outputs.dir(artifactsDir) 60 | dependsOn(tasks.withType(Jar)) 61 | } 62 | 63 | gradle.taskGraph.whenReady { 64 | List filesToChecksum = [] 65 | publishing.publications.withType(MavenPublication).configureEach { 66 | artifacts.each { 67 | if (it.file.name == 'grails-plugin.xml' || it.file.name == 'profile.yml') { 68 | return 69 | } 70 | filesToChecksum << it.file 71 | } 72 | } 73 | 74 | checksumTask.configure { Checksum check -> 75 | check.inputFiles.from = filesToChecksum.unique() 76 | check.finalizedBy(artifactsTask) 77 | } 78 | 79 | artifactsTask.configure { 80 | doLast { 81 | Map artifacts = [:] 82 | publishing.publications.withType(MavenPublication).configureEach { mavenPublication -> 83 | mavenPublication.artifacts.each { artifact -> 84 | if (!artifact.file.exists() || artifact.file.name == 'grails-plugin.xml' || artifact.file.name == 'profile.yml') { 85 | return 86 | } 87 | if(artifact.classifier) { 88 | artifacts[artifact.file.name] = "$groupId:$artifactId:$version:$artifact.classifier".toString() 89 | } 90 | else { 91 | artifacts[artifact.file.name] = "$groupId:$artifactId:$version".toString() 92 | } 93 | } 94 | } 95 | 96 | File artifactsFile = artifactsDir.get().asFile 97 | artifactsFile.mkdirs() 98 | 99 | artifacts.each { 100 | File published = new File(artifactsFile, "${it.key}.txt") 101 | published.text = it.value 102 | } 103 | } 104 | } 105 | } 106 | 107 | Set publishTasks = tasks.names.findAll { it.startsWith('publishMavenPublication') } 108 | publishTasks.each { taskName -> 109 | tasks.named(taskName) { 110 | finalizedBy(checksumTask) 111 | } 112 | } 113 | 114 | 115 | def aggregatePublishedArtifacts = tasks.register('aggregatePublishedArtifacts') 116 | 117 | tasks.register("aggregateChecksums").configure { 118 | group = "publishing" 119 | description = "Aggregates all SHA-256 checksums into a single file." 120 | 121 | def outputFileProvider = rootProject.layout.buildDirectory.file("CHECKSUMS.txt") 122 | outputs.file(outputFileProvider) 123 | 124 | dependsOn(checksumTask) 125 | finalizedBy(aggregatePublishedArtifacts) 126 | 127 | outputs.upToDateWhen { false } // not worth caching 128 | 129 | doLast { 130 | def outputFile = outputFileProvider.get().asFile 131 | outputFile.withPrintWriter { writer -> 132 | def checksumDir = layout.buildDirectory.dir("checksums").get().asFile 133 | if (checksumDir.exists()) { 134 | checksumDir.listFiles(new FilenameFilter() { 135 | boolean accept(File dir, String name) { 136 | return name.endsWith(".sha512") 137 | } 138 | })?.each { checksumFile -> 139 | def jarName = checksumFile.name - ".sha512" 140 | def checksumLine = checksumFile.text.trim() 141 | def checksum = checksumLine.tokenize()[0] 142 | writer.println("${jarName} ${checksum}") 143 | } 144 | } 145 | } 146 | 147 | println "Checksum manifest written to ${outputFile}" 148 | } 149 | } 150 | 151 | aggregatePublishedArtifacts.configure { 152 | group = "publishing" 153 | description = "Aggregates all published artifacts into a single file." 154 | 155 | def outputFileProvider = rootProject.layout.buildDirectory.file("PUBLISHED_ARTIFACTS.txt") 156 | outputs.file(outputFileProvider) 157 | 158 | outputs.upToDateWhen { false } // not worth caching 159 | 160 | dependsOn(artifactsTask) 161 | 162 | doLast { 163 | def outputFile = outputFileProvider.get().asFile 164 | outputFile.text = "" // clear previous 165 | outputFile.withPrintWriter { writer -> 166 | def publishedArtifactsDir = artifactsDir.get().asFile 167 | if (publishedArtifactsDir.exists()) { 168 | publishedArtifactsDir.listFiles(new FilenameFilter() { 169 | boolean accept(File dir, String name) { 170 | return name.endsWith(".txt") 171 | } 172 | })?.each { checksumFile -> 173 | def artifactName = checksumFile.name - ".txt" 174 | def coordinates = checksumFile.text.trim() 175 | writer.println("${artifactName} ${coordinates}") 176 | } 177 | } 178 | } 179 | 180 | println "Published artifacts written to ${outputFile}" 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | --------------------------------------------------------------------------------