├── .github ├── release-drafter.yml ├── renovate.json └── workflows │ ├── gradle.yml │ ├── release-notes.yml │ └── release.yml ├── .gitignore ├── .sdkmanrc ├── LICENSE ├── README.md ├── build.gradle ├── buildSrc └── build.gradle ├── gradle.properties ├── gradle ├── docs-config.gradle ├── java-config.gradle ├── publish-config.gradle ├── test-config.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── grails-app ├── conf │ └── plugin.yml └── services │ └── grails │ └── plugins │ └── quartz │ └── JobManagerService.groovy ├── settings.gradle └── src ├── docs ├── guide │ ├── configuration.gdoc │ ├── introduction.gdoc │ ├── scheduling.gdoc │ ├── toc.yml │ └── triggers.gdoc └── ref │ ├── Command Line │ └── create-job.gdoc │ └── Triggers │ ├── cron.gdoc │ ├── custom.gdoc │ └── simple.gdoc ├── main ├── groovy │ ├── grails │ │ └── plugins │ │ │ └── quartz │ │ │ ├── CustomTriggerFactoryBean.java │ │ │ ├── DefaultGrailsJobClass.java │ │ │ ├── GrailsJobClass.java │ │ │ ├── GrailsJobClassConstants.java │ │ │ ├── GrailsJobFactory.java │ │ │ ├── JobArtefactHandler.groovy │ │ │ ├── JobDescriptor.groovy │ │ │ ├── JobDetailFactoryBean.java │ │ │ ├── QuartzJob.groovy │ │ │ ├── QuartzJobTraitInjector.groovy │ │ │ ├── TriggerDescriptor.groovy │ │ │ ├── TriggerUtils.groovy │ │ │ ├── cleanup │ │ │ └── JdbcCleanup.groovy │ │ │ ├── config │ │ │ └── TriggersConfigBuilder.groovy │ │ │ └── listeners │ │ │ ├── ExceptionPrinterJobListener.java │ │ │ └── SessionBinderJobListener.java │ └── quartz │ │ └── QuartzGrailsPlugin.groovy ├── scripts │ └── CreateJob.groovy └── templates │ └── Job.groovy └── test └── groovy └── grails └── plugins └── quartz ├── CustomTriggerFactoryBeanSpec.groovy ├── DefaultGrailsJobClassSpec.groovy ├── JobArtefactHandlerSpec.groovy ├── JobDescriptorSpec.groovy ├── JobDetailFactoryBeanSpec.groovy ├── MockDoWithSpring.groovy ├── QuartzJobTraitInjectorSpec.groovy ├── TestQuartzJob.groovy ├── TriggerDescriptorSpec.groovy └── config └── TriggersConfigBuilderSpec.groovy /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: $RESOLVED_VERSION 2 | tag-template: v$RESOLVED_VERSION 3 | pull-request: 4 | title-templates: 5 | fix: '🐛 $TITLE (#$NUMBER)' 6 | feat: '🚀 $TITLE (#$NUMBER)' 7 | default: '$TITLE (#$NUMBER)' 8 | autolabeler: 9 | - label: 'bug' 10 | branch: 11 | - '/fix\/.+/' 12 | title: 13 | - '/fix/i' 14 | - label: 'improvement' 15 | branch: 16 | - '/improv\/.+/' 17 | title: 18 | - '/improv/i' 19 | - label: 'feature' 20 | branch: 21 | - '/feature\/.+/' 22 | title: 23 | - '/feat/i' 24 | - label: 'documentation' 25 | branch: 26 | - '/docs\/.+/' 27 | title: 28 | - '/docs/i' 29 | - label: 'maintenance' 30 | branch: 31 | - '/(chore|refactor|style|test|ci|perf|build)\/.+/' 32 | title: 33 | - '/(chore|refactor|style|test|ci|perf|build)/i' 34 | - label: 'chore' 35 | branch: 36 | - '/chore\/.+/' 37 | title: 38 | - '/chore/i' 39 | - label: 'refactor' 40 | branch: 41 | - '/refactor\/.+/' 42 | title: 43 | - '/refactor/i' 44 | - label: 'style' 45 | branch: 46 | - '/style\/.+/' 47 | title: 48 | - '/style/i' 49 | - label: 'test' 50 | branch: 51 | - '/test\/.+/' 52 | title: 53 | - '/test/i' 54 | - label: 'ci' 55 | branch: 56 | - '/ci\/.+/' 57 | title: 58 | - '/ci/i' 59 | - label: 'perf' 60 | branch: 61 | - '/perf\/.+/' 62 | title: 63 | - '/perf/i' 64 | - label: 'build' 65 | branch: 66 | - '/build\/.+/' 67 | title: 68 | - '/build/i' 69 | - label: 'deps' 70 | branch: 71 | - '/deps\/.+/' 72 | title: 73 | - '/deps/i' 74 | - label: 'revert' 75 | branch: 76 | - '/revert\/.+/' 77 | title: 78 | - '/revert/i' 79 | categories: 80 | - title: '🚀 Features' 81 | labels: 82 | - 'feature' 83 | - "type: enhancement" 84 | - "type: new feature" 85 | - "type: major" 86 | - "type: minor" 87 | - title: '💡 Improvements' 88 | labels: 89 | - 'improvement' 90 | - "type: improvement" 91 | 92 | - title: '🐛 Bug Fixes' 93 | labels: 94 | - 'fix' 95 | - 'bug' 96 | - "type: bug" 97 | - title: '📚 Documentation' 98 | labels: 99 | - 'docs' 100 | - title: '🔧 Maintenance' 101 | labels: 102 | - 'maintenance' 103 | - 'chore' 104 | - 'refactor' 105 | - 'style' 106 | - 'test' 107 | - 'ci' 108 | - 'perf' 109 | - 'build' 110 | - "type: ci" 111 | - "type: build" 112 | - title: '⏪ Reverts' 113 | labels: 114 | - 'revert' 115 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 116 | version-resolver: 117 | major: 118 | labels: 119 | - 'type: major' 120 | minor: 121 | labels: 122 | - 'type: minor' 123 | patch: 124 | labels: 125 | - 'type: patch' 126 | default: patch 127 | template: | 128 | ## What's Changed 129 | 130 | $CHANGES 131 | 132 | ## Contributors 133 | 134 | $CONTRIBUTORS -------------------------------------------------------------------------------- /.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/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: "Java CI" 2 | on: 3 | push: 4 | branches: 5 | - '[3-9]+.[0-9]+.x' 6 | pull_request: 7 | branches: 8 | - '[3-9]+.[0-9]+.x' 9 | workflow_dispatch: 10 | permissions: 11 | packages: read 12 | jobs: 13 | test_project: 14 | name: "Test Project" 15 | if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - name: "📥 Checkout repository" 19 | uses: actions/checkout@v4 20 | - name: "☕️ Setup JDK" 21 | uses: actions/setup-java@v4 22 | with: 23 | java-version: 17 24 | distribution: liberica 25 | - name: "🐘 Setup Gradle" 26 | uses: gradle/actions/setup-gradle@v4 27 | with: 28 | develocity-access-key: ${{ secrets.GRAILS_DEVELOCITY_ACCESS_KEY }} 29 | - name: "🏃‍♂️ Run Tests" 30 | run: ./gradlew check --continue 31 | publish_snapshot: 32 | name: "Build Project and Publish Snapshot" 33 | runs-on: ubuntu-22.04 34 | if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' 35 | permissions: 36 | contents: write 37 | steps: 38 | - name: "📥 Checkout repository" 39 | uses: actions/checkout@v4 40 | - name: "☕️ Setup JDK" 41 | uses: actions/setup-java@v4 42 | with: 43 | java-version: 17 44 | distribution: liberica 45 | - name: "🐘 Setup Gradle" 46 | uses: gradle/actions/setup-gradle@v4 47 | with: 48 | develocity-access-key: ${{ secrets.GRAILS_DEVELOCITY_ACCESS_KEY }} 49 | - name: "📤 Publish Snapshot artifacts" 50 | env: 51 | GITHUB_MAVEN_PASSWORD: ${{ secrets.GITHUB_TOKEN }} 52 | GRAILS_PUBLISH_RELEASE: 'false' 53 | MAVEN_PUBLISH_USERNAME: ${{ secrets.NEXUS_USER }} 54 | MAVEN_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PW }} 55 | MAVEN_PUBLISH_URL: ${{ secrets.GRAILS_NEXUS_PUBLISH_SNAPSHOT_URL }} 56 | run: ./gradlew --no-build-cache publish 57 | - name: "🔨 Generate Snapshot Documentation" 58 | run: ./gradlew docs 59 | - name: "🚀 Publish to Github Pages" 60 | uses: apache/grails-github-actions/deploy-github-pages@asf 61 | env: 62 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | GRADLE_PUBLISH_RELEASE: 'false' 64 | SOURCE_FOLDER: build/docs/manual 65 | -------------------------------------------------------------------------------- /.github/workflows/release-notes.yml: -------------------------------------------------------------------------------- 1 | name: "Release Drafter" 2 | on: 3 | issues: 4 | types: [closed,reopened] 5 | push: 6 | branches: 7 | - '[3-9]+.[0-9]+.x' 8 | pull_request: 9 | types: [opened, reopened, synchronize] 10 | pull_request_target: 11 | types: [opened, reopened, synchronize] 12 | workflow_dispatch: 13 | jobs: 14 | update_release_draft: 15 | permissions: 16 | contents: read # limit to read access 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: "📝 Update Release Draft" 20 | uses: release-drafter/release-drafter@v6 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GRAILS_GH_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: [published] 5 | permissions: 6 | contents: write 7 | packages: read 8 | jobs: 9 | release: 10 | runs-on: ubuntu-24.04 11 | permissions: 12 | packages: read # pre-release workflow 13 | contents: write # to create release 14 | issues: write # to modify milestones 15 | steps: 16 | - name: "📥 Checkout repository" 17 | uses: actions/checkout@v4 18 | - name: "☕️ Setup JDK" 19 | uses: actions/setup-java@v4 20 | with: 21 | java-version: 17 22 | distribution: liberica 23 | - name: "🐘 Setup Gradle" 24 | uses: gradle/actions/setup-gradle@v4 25 | with: 26 | develocity-access-key: ${{ secrets.GRAILS_DEVELOCITY_ACCESS_KEY }} 27 | - name: "📝 Store the current release version" 28 | id: release_version 29 | run: echo "release_version=${GITHUB_REF:11}" >> $GITHUB_OUTPUT 30 | - name: "⚙️ Run pre-release" 31 | uses: apache/grails-github-actions/pre-release@asf 32 | - name: "🔐 Generate key file for artifact signing" 33 | env: 34 | SECRING_FILE: ${{ secrets.SECRING_FILE }} 35 | run: echo $SECRING_FILE | base64 -d > ${{ github.workspace }}/secring.gpg 36 | - name: "📤 Publish to Sonatype - close and release staging repository" 37 | env: 38 | GITHUB_MAVEN_PASSWORD: ${{ secrets.GITHUB_TOKEN }} 39 | GRAILS_PUBLISH_RELEASE: 'true' 40 | NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_USER }} 41 | NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PW }} 42 | NEXUS_PUBLISH_URL: ${{ secrets.GRAILS_NEXUS_PUBLISH_RELEASE_URL }} 43 | NEXUS_PUBLISH_STAGING_PROFILE_ID: ${{ secrets.NEXUS_PUBLISH_STAGING_PROFILE_ID }} # TODO: What about this secret? 44 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 45 | SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} 46 | run: > 47 | ./gradlew --refresh-dependencies 48 | -Psigning.secretKeyRingFile=${{ github.workspace }}/secring.gpg 49 | publishToSonatype 50 | closeAndReleaseSonatypeStagingRepository 51 | - name: "🔨 Build Documentation" 52 | run: ./gradlew docs 53 | - name: "🚀 Publish to Github Pages" 54 | uses: apache/grails-github-actions/deploy-github-pages@asf 55 | env: 56 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | GRADLE_PUBLISH_RELEASE: 'true' 58 | SOURCE_FOLDER: build/docs/manual 59 | VERSION: ${{ steps.release_version.outputs.release_version }} 60 | - name: "⚙️ Run post-release" 61 | uses: apache/grails-github-actions/post-release@asf 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | .idea/ 3 | build/ 4 | classes/ 5 | .project 6 | .settings 7 | .classpath 8 | *.iml 9 | *.ipr 10 | *.iws 11 | -------------------------------------------------------------------------------- /.sdkmanrc: -------------------------------------------------------------------------------- 1 | # Enable auto-env through the sdkman_auto_env config - https://sdkman.io/usage#env 2 | java=17.0.14-librca -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grails Quartz Plugin 2 | 3 | [![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) 4 | [![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) 5 | 6 | ## Documentation 7 | 8 | [Latest documentation](https://apache.github.io/grails-quartz/latest/) and [snapshots](https://apache.github.io/grails-quartz/snapshot/) are available. 9 | 10 | ## Branches 11 | 12 | | Branch | Grails Version | 13 | |--------|----------------| 14 | | 1.x | 2 | 15 | | 2.0.x | 3-5 | 16 | | 3.0.x | 6 | 17 | | 4.0.x | 7 | 18 | 19 | ## Using 20 | ### Quick start 21 | To start using Quartz plugin just simply add 22 | `implementation 'org.apache.grails:grails-quartz:{version}'` in your `build.gradle`. 23 | 24 | >[!NOTE] 25 | > __2.0.13 for Grails 3.3.*__\ 26 | > Properties changed to `static` from `def`.\ 27 | > For example: `def concurrent` will be now `static concurrent`. 28 | 29 | ### Scheduling Jobs 30 | 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: 31 | ```groovy 32 | package com.mycompany.myapp 33 | 34 | class MyJob { 35 | 36 | static triggers = { 37 | simple repeatInterval: 1000 38 | } 39 | 40 | void execute() { 41 | println 'Job run!' 42 | } 43 | } 44 | ``` 45 | 46 | The above example will call the `execute()` method every second. 47 | 48 | ### Scheduling configuration syntax 49 | 50 | Currently, plugin supports three types of [triggers](http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/tutorial-lesson-02.html): 51 | * **simple trigger** — executes once per defined interval (ex. "every 10 seconds"); 52 | * **cron trigger** — executes job with cron expression (ex. "at 8:00 am every Monday through Friday"); 53 | * **custom trigger** — your implementation of [Trigger](http://www.quartz-scheduler.org/api/2.3.0/org/quartz/Trigger.html) interface. 54 | 55 | Multiple triggers per job are allowed. 56 | ```groovy 57 | class MyJob { 58 | 59 | static triggers = { 60 | simple name: 'simpleTrigger', startDelay: 10000, repeatInterval: 30000, repeatCount: 10 61 | cron name: 'cronTrigger', startDelay: 10000, cronExpression: '0/6 * 15 * * ?' 62 | custom name: 'customTrigger', triggerClass: MyTriggerClass, myParam: myValue, myAnotherParam: myAnotherValue 63 | } 64 | 65 | void execute() { 66 | println 'Job run!' 67 | } 68 | } 69 | ``` 70 | 71 | 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. 72 | 73 | Three kinds of triggers are supported with the following parameters. The name field must be unique: 74 | * `simple`: 75 | * `name` — the name that identifies the trigger; 76 | * `startDelay` — delay (in milliseconds) between scheduler startup and first job's execution; 77 | * `repeatInterval` — timeout (in milliseconds) between consecutive job's executions; 78 | * `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); 79 | * `cron`: 80 | * `name` — the name that identifies the trigger; 81 | * `startDelay` — delay (in milliseconds) between scheduler startup and first job's execution; 82 | * `cronExpression` — [cron expression](http://www.quartz-scheduler.org/api/2.2.0/org/quartz/CronExpression.html) 83 | * `custom`: 84 | * `triggerClass` — your class which implements [CalendarIntervalTriggerImpl](http://www.quartz-scheduler.org/api/2.3.0/org/quartz/impl/triggers/CalendarIntervalTriggerImpl.html) impl; 85 | * any params needed by your trigger. 86 | 87 | ### Configuration plugin syntax 88 | 89 | You can add the following properties to control persistence or not persistence: 90 | * `quartz.pluginEnabled` - defaults to `true`, can disable plugin for test cases etc 91 | * `quartz.jdbcStore` - `true` to enable database store, `false` to use RamStore (default: `true`) 92 | * `quartz.autoStartup` - delays jobs until after bootstrap startup phase (default: `false`) 93 | * `quartz.jdbcStoreDataSource` - jdbc data source alternate name 94 | * `quartz.waitForJobsToCompleteOnShutdown` - wait for jobs to complete on shutdown (default: `true`) 95 | * `quartz.exposeSchedulerInRepository` - expose Schedule in repository 96 | * `quartz.scheduler.instanceName` - name of the scheduler to avoid conflicts between apps 97 | * `quartz.purgeQuartzTablesOnStartup` - when jdbcStore set to `true` and this is `true`, clears out all quartz tables on startup 98 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'org.apache.grails.gradle.grails-plugin' 4 | } 5 | 6 | version = projectVersion 7 | group = 'org.apache.grails' 8 | 9 | repositories { 10 | mavenCentral() 11 | maven { url = 'https://repo.grails.org/grails/core' } 12 | maven { url = 'https://repository.apache.org/content/repositories/snapshots' } 13 | } 14 | 15 | dependencies { 16 | 17 | implementation platform("org.apache.grails:grails-bom:$grailsVersion") 18 | 19 | api "org.quartz-scheduler:quartz:$quartzVersion", { 20 | // api: AdaptableJobFactory, InterruptableJob, JobExecutionContext, JobExecutionException, 21 | // UnableToInterruptJobException, TriggerFiredBundle, 22 | // runtime: @DisallowConcurrentExecution(runtime), @PersistJobDataAfterExecution(runtime) 23 | } 24 | 25 | api 'org.springframework:spring-beans', { 26 | // api: BeansException 27 | // impl: BeanUtils, BeanWrapper, FactoryBean, InitializingBean, PropertyAccessorFactory 28 | } 29 | api 'org.springframework:spring-context', { 30 | // api: ApplicationContext 31 | // impl: ApplicationContextAware 32 | } 33 | 34 | implementation 'org.apache.groovy:groovy-sql', { 35 | // impl: Sql 36 | } 37 | implementation 'org.springframework:spring-context-support', { 38 | // impl: AdaptableJobFactory 39 | } 40 | implementation 'org.springframework:spring-core', { 41 | // impl: ReflectionUtils 42 | } 43 | 44 | compileOnly 'org.apache.grails:grails-core', { // Provided as this is a Grails Plugin 45 | // api: PersistenceContextInterceptor 46 | // impl: AbstractGrailsClass, GrailsClass, GrailsClassUtils 47 | } 48 | compileOnly 'org.apache.groovy:groovy' // Provided as this is a Grails Plugin 49 | compileOnly 'org.slf4j:slf4j-api', { 50 | // api: Logger, LoggerFactory 51 | } 52 | 53 | testImplementation 'org.apache.grails:grails-core', { 54 | // impl: GrailsClass 55 | } 56 | testImplementation 'org.spockframework:spock-core' 57 | } 58 | 59 | apply { 60 | from layout.projectDirectory.file('gradle/docs-config.gradle') 61 | from layout.projectDirectory.file('gradle/java-config.gradle') 62 | from layout.projectDirectory.file('gradle/publish-config.gradle') 63 | from layout.projectDirectory.file('gradle/test-config.gradle') 64 | } -------------------------------------------------------------------------------- /buildSrc/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'groovy-gradle-plugin' 3 | } 4 | 5 | Properties versions = new Properties() 6 | file('../gradle.properties').withInputStream { 7 | versions.load(it) 8 | } 9 | 10 | repositories { 11 | mavenCentral() 12 | maven { url = 'https://repo.grails.org/grails/core' } 13 | maven { url = 'https://repository.apache.org/content/repositories/snapshots' } 14 | } 15 | 16 | dependencies { 17 | implementation platform("org.apache.grails:grails-bom:${versions.get('grailsVersion')}") 18 | implementation 'org.apache.grails:grails-gradle-plugins' 19 | } 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | projectVersion=4.0.0-SNAPSHOT 2 | 3 | grailsVersion=7.0.0-SNAPSHOT 4 | javaVersion=17 5 | quartzVersion=2.5.0 6 | 7 | # This prevents the Grails Gradle Plugin from unnecessarily excluding slf4j-simple in the generated POMs 8 | # https://github.com/apache/grails-gradle-plugin/issues/222 9 | slf4jPreventExclusion=true 10 | 11 | org.gradle.caching=true 12 | org.gradle.daemon=true 13 | org.gradle.parallel=true 14 | -------------------------------------------------------------------------------- /gradle/docs-config.gradle: -------------------------------------------------------------------------------- 1 | import org.grails.gradle.plugin.doc.PublishGuideTask 2 | 3 | apply plugin: 'org.apache.grails.gradle.grails-docs' 4 | 5 | configurations.register('groovydocConfiguration') 6 | configurations.register('guideConfiguration') 7 | 8 | dependencies { 9 | 10 | groovydocConfiguration localGroovy(), { 11 | because 'groovydoc needs to run with the same version as Gradle' 12 | } 13 | 14 | guideConfiguration 'org.apache.grails:grails-docs-core' 15 | guideConfiguration 'org.apache.groovy:groovy-ant' 16 | guideConfiguration 'org.apache.groovy:groovy-templates' 17 | } 18 | 19 | tasks.withType(Groovydoc).configureEach { 20 | access = GroovydocAccess.PROTECTED 21 | processScripts = false 22 | includeMainForScripts = false 23 | includeAuthor = true 24 | classpath = configurations.groovydocConfiguration 25 | groovyClasspath = configurations.groovydocConfiguration 26 | } 27 | 28 | tasks.withType(PublishGuideTask).configureEach { 29 | classpath = configurations.guideConfiguration 30 | } 31 | 32 | tasks.named('docs') { 33 | group = 'documentation' 34 | } 35 | -------------------------------------------------------------------------------- /gradle/java-config.gradle: -------------------------------------------------------------------------------- 1 | compileJava.options.release = javaVersion.toInteger() -------------------------------------------------------------------------------- /gradle/publish-config.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'org.apache.grails.gradle.grails-publish' 2 | 3 | grailsPublish { 4 | githubSlug = 'apache/grails-quartz' 5 | license.name = 'Apache-2.0' 6 | title = 'Grails Quartz Plugin' 7 | desc = 'This plugin allows your Grails application to schedule jobs to be executed using a specified interval or cron expression.' 8 | developers = [ 9 | burtbeckwith: 'Burt Beckwith', 10 | jeffscottbrown: 'Jeff Scott Brown', 11 | graemerocher: 'Graeme Rocher', 12 | ryanvanderwerf: 'Ryan Vanderwerf', 13 | sergeynebolsin: 'Sergey Nebolsin', 14 | puneetbehl: 'Puneet Behl', 15 | vitaliisamolovskikh: 'Vitalii Samolovskikh' 16 | ] 17 | } -------------------------------------------------------------------------------- /gradle/test-config.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | add('testRuntimeOnly', 'org.slf4j:slf4j-nop') // Get rid of warning about missing slf4j implementation during test task 3 | add('testRuntimeOnly', 'org.junit.platform:junit-platform-launcher') // Gradle 9+ requires this 4 | } 5 | 6 | tasks.withType(Test).configureEach { 7 | useJUnitPlatform() 8 | testLogging { 9 | exceptionFormat = 'full' 10 | events 'passed', 'skipped', 'failed'//, 'standardOut', 'standardError' 11 | } 12 | } 13 | 14 | tasks.matching { it.name ==~ /^boot(Test)?Run$/ }.configureEach { 15 | // No application class is available 16 | enabled = false 17 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-quartz/daa98e49e79eb34c7ba11550142d77d98819ea51/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /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 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /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=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 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 | -------------------------------------------------------------------------------- /grails-app/conf/plugin.yml: -------------------------------------------------------------------------------- 1 | quartz: 2 | jdbcStore: false 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.gradle.develocity' version '4.0' 3 | id 'com.gradle.common-custom-user-data-gradle-plugin' version '2.2.1' 4 | } 5 | 6 | def isCI = System.getenv().containsKey('CI') 7 | def isLocal = !isCI 8 | 9 | develocity { 10 | server = 'https://ge.grails.org' 11 | buildScan { 12 | tag('grails') 13 | tag('grails-quartz') 14 | publishing.onlyIf { it.authenticated } 15 | uploadInBackground = isLocal 16 | } 17 | } 18 | 19 | buildCache { 20 | local { enabled = isLocal } 21 | remote(develocity.buildCache) { 22 | push = isCI 23 | enabled = true 24 | } 25 | } 26 | 27 | rootProject.name = 'grails-quartz' 28 | -------------------------------------------------------------------------------- /src/docs/guide/configuration.gdoc: -------------------------------------------------------------------------------- 1 | h4. Configuring the plugin 2 | 3 | The plugin supports configuration settings defined in grails-app/conf/application.yml. 4 | 5 | {code} 6 | --- 7 | quartz: 8 | autoStartup: true 9 | {code} 10 | 11 | Currently supported options: 12 | 13 | * @autoStartup@ controls automatic startup of the Quartz scheduler during application bootstrap (default: true ) 14 | * @jdbcStore@ set to true if you want Quartz to persist jobs in your DB (default: false ), you'll also need to provide quartz.properties file and make sure that required tables exist in your db (see Clustering section below for the sample config and automatic tables creation using Hibernate) 15 | 16 | h4. Logging 17 | 18 | A log is auto-injected into your task Job class without having to enable it. To set the logging level, just add something like this to your grails-app/conf/Config.groovy log4j configuration. 19 | 20 | {code} 21 | debug 'grails.app.jobs' 22 | {code} 23 | 24 | h4. Hibernate Sessions and Jobs 25 | 26 | Jobs are configured by default to have Hibernate Session bounded to thread each time job is executed. This is required if you are using Hibernate code 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: 27 | 28 | {code} 29 | static sessionRequired = false 30 | {code} 31 | 32 | h4. Configuring concurrent execution 33 | 34 | By default Jobs are executed in concurrent fashion, so new Job execution can start even if 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): 35 | 36 | {code} 37 | static concurrent = false 38 | {code} 39 | 40 | h4. Configuring Job Enabled 41 | 42 | 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 43 | 44 | {code} 45 | static jobEnabled = false 46 | {code} 47 | 48 | h4. Configuring description 49 | 50 | 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. 51 | 52 | {code} 53 | static description = "Example Job Description" 54 | {code} 55 | 56 | h4. Clustering 57 | 58 | Quartz plugin doesn't support clustering out-of-the-box now. However, you could use standard Quartz clustering configuration. Take a look at the [example provided by Burt Beckwith|http://docs.apache.org/download/attachments/78053/clustering_sample.tar.gz?version=1]. You'll also need to set jdbcStore configuration option to true . 59 | 60 | 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). Volatile job and trigger will not persist between Quartz runs, and durable job will live even when there is no triggers referring to it. 61 | 62 | Read Quartz documentation for more information on clustering and job stores as well as volatility and durability. 63 | 64 | Now that the plugin supports Quartz 2.1.x, you can now use current versions of open source Terracotta see https://github.com/rvanderwerf/terracotta-grails-demo for an example app. 65 | 66 | h4. Recovering 67 | 68 | Since 0.4.2 recovering from 'recovery' or 'fail-over' situation is supported with requestsRecovery job-level flag ( false by default). 69 | 70 | If a job "requests recovery", and it is executing during the time of 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. 71 | -------------------------------------------------------------------------------- /src/docs/guide/introduction.gdoc: -------------------------------------------------------------------------------- 1 | Quartz plugin allows your Grails application to schedule jobs to be executed using a specified interval or cron expression. The underlying system uses the [Quartz Enterprise Job Scheduler|http://www.quartz-scheduler.org/] configured via Spring, but is made simpler by the coding by convention paradigm. 2 | Since 1.0-RC3 this plugin requires Quartz 2.1.x or higher and no longer supports Quartz 1.8.x. 3 | -------------------------------------------------------------------------------- /src/docs/guide/scheduling.gdoc: -------------------------------------------------------------------------------- 1 | h4. Scheduling Jobs 2 | 3 | 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: 4 | 5 | {code} 6 | class MyJob { 7 | static triggers = { 8 | simple name: 'mySimpleTrigger', startDelay: 60000, repeatInterval: 1000 9 | } 10 | static group = "MyGroup" 11 | static description = "Example job with Simple Trigger" 12 | 13 | void execute() { 14 | println "Job run!" 15 | } 16 | } 17 | {code} 18 | 19 | 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. 20 | The triggers name property must be unique across all triggers in the application. 21 | 22 | By default, jobs will not be executed when running under the test environment. 23 | 24 | h4. Scheduling a Cron Job 25 | 26 | 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): 27 | 28 | {code} 29 | class MyJob { 30 | static triggers = { 31 | cron name: 'myTrigger', cronExpression: "0 0 6 * * ?" 32 | } 33 | static group = "MyGroup" 34 | static description = "Example job with Cron Trigger" 35 | 36 | void execute() { 37 | println "Job run!" 38 | } 39 | } 40 | {code} 41 | 42 | The fields in the cronExpression are: (summarizing the Quartz CronTrigger Tutorial) 43 | 44 | {code} 45 | cronExpression: "s m h D M W Y" 46 | | | | | | | `- Year [optional] 47 | | | | | | `- Day of Week, 1-7 or SUN-SAT, ? 48 | | | | | `- Month, 1-12 or JAN-DEC 49 | | | | `- Day of Month, 1-31, ? 50 | | | `- Hour, 0-23 51 | | `- Minute, 0-59 52 | `- Second, 0-59 53 | {code} 54 | 55 | {note} 56 | * Year is the only optional field and may be omitted, the rest are mandatory 57 | * Day-of-Week and Month are case insensitive, so "DEC" = "dec" = "Dec" 58 | * 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. 59 | * See the CronTrigger Tutorial for an explanation of all the special characters you may use. 60 | {note} 61 | -------------------------------------------------------------------------------- /src/docs/guide/toc.yml: -------------------------------------------------------------------------------- 1 | introduction: Introduction 2 | scheduling: Scheduling Basics 3 | triggers: Understanding Triggers 4 | configuration: Plugin Configuration 5 | -------------------------------------------------------------------------------- /src/docs/guide/triggers.gdoc: -------------------------------------------------------------------------------- 1 | h4. Scheduling configuration syntax 2 | 3 | Currently plugin supports three types of triggers: 4 | 5 | * *simple* — executes once per defined interval (ex. “every 10 seconds”); 6 | * *cron* — executes job with cron expression (ex. “at 8:00am every Monday through Friday”); 7 | * *custom* — your implementation of Trigger interface. 8 | 9 | Multiple triggers per job are allowed. 10 | 11 | {code} 12 | class MyJob { 13 | static triggers = { 14 | simple name:'simpleTrigger', startDelay: 10000, repeatInterval: 30000, repeatCount: 10 15 | cron name:'cronTrigger', startDelay: 10000, cronExpression: '0/6 * 15 * * ?', timeZone: TimeZone.getTimeZone("GMT-8") //timeZone is optional 16 | custom name:'customTrigger', triggerClass: MyTriggerClass, myParam: myValue, myAnotherParam: myAnotherValue 17 | } 18 | 19 | void execute() { 20 | println "Job run!" 21 | } 22 | } 23 | {code} 24 | 25 | 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. 26 | 27 | Three kinds of triggers are supported with the following parameters: 28 | 29 | * @simple@: 30 | ** @name@ — the name that identifies the trigger; 31 | ** @startDelay@ — delay (in milliseconds) between scheduler startup and first job’s execution; 32 | ** @repeatInterval@ — timeout (in milliseconds) between consecutive job’s executions; 33 | ** @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); 34 | * @cron@: 35 | ** @name@ — the name that identifies the trigger; 36 | ** @startDelay@ — delay (in milliseconds) between scheduler startup and first job’s execution; 37 | ** @cronExpression@ — cron expression 38 | * @custom@: 39 | ** @triggerClass@ — your class which implements Trigger interface; 40 | any params needed by your trigger. 41 | 42 | 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. 43 | 44 | h4. Dynamic Jobs Scheduling 45 | 46 | Starting from 0.4.1 version you have the ability to schedule job executions dynamically. 47 | 48 | These methods are available: 49 | 50 | {code} 51 | // creates cron trigger; 52 | MyJob.schedule(String cronExpression, Map params?) 53 | 54 | // creates simple trigger: repeats job repeatCount+1 times with delay of repeatInterval milliseconds; 55 | MyJob.schedule(Long repeatInterval, Integer repeatCount?, Map params?) ) 56 | 57 | // schedules one job execution to the specific date; 58 | MyJob.schedule(Date scheduleDate, Map params?) 59 | 60 | //schedules job's execution with a custom trigger; 61 | MyJob.schedule(Trigger trigger) 62 | 63 | // force immediate execution of the job. 64 | MyJob.triggerNow(Map params?) 65 | 66 | // Each method (except the one for custom trigger) takes optional 'params' argument. 67 | // You can use it to pass some data to your job and then access it from the job: 68 | class MyJob { 69 | void execute(context) { 70 | println context.mergedJobDataMap.foo 71 | } 72 | } 73 | // now in your controller (or service, or something else): 74 | 75 | MyJob.triggerNow([foo:"It Works!"]) 76 | {code} 77 | -------------------------------------------------------------------------------- /src/docs/ref/Command Line/create-job.gdoc: -------------------------------------------------------------------------------- 1 | h1. create-job 2 | 3 | h2. Purpose 4 | 5 | Creates a job class 6 | 7 | h2. Examples 8 | 9 | {code} 10 | $ grails create-job MyJob 11 | {code} 12 | -------------------------------------------------------------------------------- /src/docs/ref/Triggers/cron.gdoc: -------------------------------------------------------------------------------- 1 | h1. cron 2 | 3 | h2. Purpose 4 | 5 | A cron trigger that executes based on the defined cron expression 6 | 7 | h2. Examples 8 | 9 | {code} 10 | class MyJob { 11 | static triggers = { 12 | cron name:'cronTrigger', startDelay:10000, cronExpression: '0/6 * 15 * * ?', timeZone: TimeZone.getTimeZone("GMT-8") //timeZone is optional 13 | } 14 | 15 | void execute() { 16 | println "Job run!" 17 | } 18 | } 19 | {code} 20 | -------------------------------------------------------------------------------- /src/docs/ref/Triggers/custom.gdoc: -------------------------------------------------------------------------------- 1 | h1. custom 2 | 3 | h2. Purpose 4 | 5 | A custom trigger that executes based on the result of a custom @Trigger@ implementation 6 | 7 | h2. Examples 8 | 9 | {code} 10 | class MyJob { 11 | static triggers = { 12 | custom name:'customTrigger', triggerClass:MyTriggerClass, myParam:myValue, myAnotherParam:myAnotherValue 13 | } 14 | 15 | void execute() { 16 | println "Job run!" 17 | } 18 | } 19 | {code} 20 | -------------------------------------------------------------------------------- /src/docs/ref/Triggers/simple.gdoc: -------------------------------------------------------------------------------- 1 | h1. simple 2 | 3 | h2. Purpose 4 | 5 | A simple trigger that executes on a set internal 6 | 7 | h2. Examples 8 | 9 | {code} 10 | class MyJob { 11 | static triggers = { 12 | simple name:'simpleTrigger', startDelay:10000, repeatInterval: 30000, repeatCount: 10 13 | } 14 | } 15 | {code} 16 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/cleanup/JdbcCleanup.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.quartz.cleanup 2 | 3 | import groovy.sql.Sql 4 | import groovy.util.logging.Slf4j 5 | import jakarta.annotation.PostConstruct; 6 | 7 | 8 | 9 | /** 10 | * Contributed by Rocketmiles 11 | * This class can purge all of the quartz tables on startup if you set the config flag quartz.purgeQuartzTablesOnStartup = true 12 | * Mostly used for testing or development purposes. It is not recommended you use this for production as you can 13 | * miss missfire recoveries, persisted jobs/triggers, etc. 14 | */ 15 | 16 | @Slf4j 17 | public class JdbcCleanup { 18 | 19 | def dataSource 20 | 21 | @PostConstruct 22 | void init() { 23 | 24 | log.info "[quartz-plugin] Purging Quartz tables...." 25 | 26 | def queries = [] 27 | queries.add("DELETE FROM QRTZ_FIRED_TRIGGERS") 28 | queries.add("DELETE FROM QRTZ_PAUSED_TRIGGER_GRPS") 29 | queries.add("DELETE FROM QRTZ_SCHEDULER_STATE") 30 | queries.add("DELETE FROM QRTZ_LOCKS") 31 | queries.add("DELETE FROM QRTZ_SIMPLE_TRIGGERS") 32 | queries.add("DELETE FROM QRTZ_SIMPROP_TRIGGERS") 33 | queries.add("DELETE FROM QRTZ_CRON_TRIGGERS") 34 | queries.add("DELETE FROM QRTZ_BLOB_TRIGGERS") 35 | queries.add("DELETE FROM QRTZ_TRIGGERS") 36 | queries.add("DELETE FROM QRTZ_JOB_DETAILS") 37 | queries.add("DELETE FROM QRTZ_CALENDARS") 38 | 39 | def sql = new Sql(dataSource) 40 | queries.each { query -> 41 | log.info("Executing " + query) 42 | sql.execute(query) 43 | } 44 | 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugins/quartz/config/TriggersConfigBuilder.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 grails.util.GrailsUtil 22 | import grails.core.GrailsApplication 23 | import org.quartz.CronExpression 24 | import org.quartz.SimpleTrigger 25 | import org.quartz.Trigger 26 | import org.quartz.impl.triggers.CronTriggerImpl 27 | import org.quartz.impl.triggers.SimpleTriggerImpl 28 | 29 | /** 30 | * Groovy Builder for parsing triggers configuration info. 31 | * 32 | * @author Sergey Nebolsin (nebolsin@gmail.com) 33 | * 34 | * @since 0.3 35 | */ 36 | class TriggersConfigBuilder extends BuilderSupport { 37 | private triggerNumber = 0 38 | private jobName 39 | GrailsApplication grailsApplication 40 | def triggers = [:] 41 | 42 | TriggersConfigBuilder(String jobName, GrailsApplication grailsApplication) { 43 | this.jobName = jobName 44 | this.grailsApplication = grailsApplication 45 | } 46 | 47 | /** 48 | * Evaluate triggers closure 49 | */ 50 | def build(closure) { 51 | closure.delegate = this 52 | closure.call() 53 | return triggers 54 | } 55 | 56 | /** 57 | * Create a trigger. 58 | * 59 | * @param name the name of the method to create trigger. It's trigger type: simple, cron, custom. 60 | * @param attributes trigger attributes 61 | * @return trigger definitions 62 | */ 63 | Expando createTrigger(name, Map attributes) { 64 | def triggerClass 65 | 66 | def triggerAttributes = attributes ? new HashMap(attributes) : [:] 67 | 68 | prepareCommonTriggerAttributes(triggerAttributes) 69 | 70 | String triggerType = normalizeTriggerType(name) 71 | 72 | switch (triggerType) { 73 | case 'simple': 74 | triggerClass = SimpleTriggerImpl 75 | prepareSimpleTriggerAttributes(triggerAttributes) 76 | break 77 | case 'cron': 78 | triggerClass = CronTriggerImpl 79 | prepareCronTriggerAttributes(triggerAttributes) 80 | break 81 | case 'custom': 82 | if (!triggerAttributes?.triggerClass) { 83 | throw new Exception("Custom trigger must have 'triggerClass' attribute") 84 | } 85 | triggerClass = (Class) triggerAttributes.remove('triggerClass') 86 | if (!Trigger.isAssignableFrom(triggerClass)){ 87 | throw new Exception("Custom trigger class must implement org.quartz.Trigger class.") 88 | } 89 | break 90 | default: 91 | throw new Exception("Invalid format") 92 | } 93 | 94 | new Expando(clazz: CustomTriggerFactoryBean, triggerClass: triggerClass, triggerAttributes: triggerAttributes) 95 | } 96 | 97 | /** 98 | * Convert old trigger types' names 99 | * 100 | * @param old or new trigger type 101 | * @return new trigger type 102 | */ 103 | private String normalizeTriggerType(name) { 104 | def triggerType = name 105 | 106 | if (triggerType == 'simpleTrigger') { 107 | GrailsUtil.deprecated("You're using deprecated 'simpleTrigger' construction in the ${jobName}, use 'simple' instead.") 108 | triggerType = 'simple' 109 | } else if (triggerType == 'cronTrigger') { 110 | GrailsUtil.deprecated("You're using deprecated 'cronTrigger' construction in the ${jobName}, use 'cron' instead.") 111 | triggerType = 'cron' 112 | } else if (triggerType == 'customTrigger') { 113 | GrailsUtil.deprecated("You're using deprecated 'customTrigger' construction in the ${jobName}, use 'custom' instead.") 114 | triggerType = 'custom' 115 | } 116 | triggerType 117 | } 118 | 119 | private prepareCommonTriggerAttributes(Map triggerAttributes) { 120 | def prepare = prepareTriggerAttribute.curry(triggerAttributes) 121 | 122 | if(triggerAttributes[Constants.NAME] == null) { 123 | triggerAttributes[Constants.NAME] = "${jobName}${triggerNumber++}".toString() 124 | } 125 | 126 | prepare(Constants.GROUP, Constants.DEFAULT_TRIGGERS_GROUP.toString()) 127 | prepare( 128 | Constants.START_DELAY, 129 | Constants.DEFAULT_START_DELAY, 130 | { 131 | if (!(it instanceof Integer || it instanceof Long)) { 132 | throw new IllegalArgumentException( 133 | "startDelay trigger property in the job class ${jobName} must be Integer or Long" 134 | ) 135 | } 136 | if (((Number) it).longValue() < 0) { 137 | throw new IllegalArgumentException( 138 | "startDelay trigger property in the job class ${jobName} is negative (possibly integer overflow error)" 139 | ) 140 | } 141 | } 142 | ) 143 | } 144 | 145 | private prepareSimpleTriggerAttributes(Map triggerAttributes) { 146 | def prepare = prepareTriggerAttribute.curry(triggerAttributes) 147 | 148 | // Process the old deprecated property "timeout" 149 | if (triggerAttributes[Constants.TIMEOUT] != null) { 150 | GrailsUtil.deprecated("You're using deprecated 'timeout' property in the ${jobName}, use 'repeatInterval' instead") 151 | 152 | if (!(triggerAttributes[Constants.TIMEOUT] instanceof Integer || triggerAttributes[Constants.TIMEOUT] instanceof Long)) { 153 | throw new IllegalArgumentException( 154 | "timeout trigger property in the job class ${jobName} must be Integer or Long" 155 | ) 156 | } 157 | if (((Number) triggerAttributes[Constants.TIMEOUT]).longValue() < 0) { 158 | throw new IllegalArgumentException( 159 | "timeout trigger property for job class ${jobName} is negative (possibly integer overflow error)" 160 | ) 161 | } 162 | triggerAttributes[Constants.REPEAT_INTERVAL] = triggerAttributes.remove(Constants.TIMEOUT) 163 | } 164 | 165 | // Validate repeat interval 166 | prepare( 167 | Constants.REPEAT_INTERVAL, 168 | Constants.DEFAULT_REPEAT_INTERVAL, 169 | { 170 | if (!(it instanceof Integer || it instanceof Long)) { 171 | throw new IllegalArgumentException("repeatInterval trigger property in the job class ${jobName} must be Integer or Long") 172 | } 173 | if (((Number) it).longValue() < 0) { 174 | throw new IllegalArgumentException("repeatInterval trigger property for job class ${jobName} is negative (possibly integer overflow error)") 175 | } 176 | } 177 | ) 178 | 179 | // Validate repeat count 180 | prepare( 181 | Constants.REPEAT_COUNT, 182 | Constants.DEFAULT_REPEAT_COUNT, 183 | { 184 | if (!(it instanceof Integer || it instanceof Long)) { 185 | throw new IllegalArgumentException( 186 | "repeatCount trigger property in the job class ${jobName} must be Integer or Long" 187 | ) 188 | } 189 | if (((Number) it).longValue() < 0 && ((Number) it).longValue() != SimpleTrigger.REPEAT_INDEFINITELY) { 190 | throw new IllegalArgumentException( 191 | "repeatCount trigger property for job class ${jobName} is negative (possibly integer overflow error)" 192 | ) 193 | } 194 | } 195 | ) 196 | } 197 | 198 | private prepareCronTriggerAttributes(Map triggerAttributes) { 199 | prepareTriggerAttribute( 200 | triggerAttributes, 201 | Constants.CRON_EXPRESSION, 202 | Constants.DEFAULT_CRON_EXPRESSION, 203 | { 204 | if (!CronExpression.isValidExpression(it.toString())) { 205 | throw new IllegalArgumentException( 206 | "Cron expression '${it}' in the job class ${jobName} is not a valid cron expression" 207 | ) 208 | } 209 | } 210 | ) 211 | } 212 | 213 | private prepareTriggerAttribute = {Map attributes, String name, defaultValue, validate = {} -> 214 | if(attributes[name] == null){ 215 | attributes[name] = defaultValue 216 | } 217 | validate(attributes[name]) 218 | } 219 | 220 | /** 221 | * Does nothing. Implements the BuilderSupport method. 222 | */ 223 | protected void setParent(parent, child) { 224 | // Nothing! 225 | } 226 | 227 | /** 228 | * Implements the BuilderSupport method. 229 | */ 230 | protected createNode(name) { 231 | createNode(name, null, null) 232 | } 233 | 234 | /** 235 | * Implements the BuilderSupport method. 236 | */ 237 | protected createNode(name, value) { 238 | createNode(name, null, value) 239 | } 240 | 241 | /** 242 | * Implements the BuilderSupport method. 243 | */ 244 | protected createNode(name, Map attributes) { 245 | createNode(name, attributes, null) 246 | } 247 | 248 | /** 249 | * Create a trigger. Implements the BuilderSupport method. 250 | */ 251 | protected Object createNode(name, Map attributes, Object value) { 252 | def trigger = createTrigger(name, attributes) 253 | triggers[trigger.triggerAttributes.name.toString()] = trigger 254 | trigger 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /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/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/main/groovy/quartz/QuartzGrailsPlugin.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-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 | package quartz 17 | 18 | import grails.plugins.Plugin 19 | import grails.plugins.quartz.* 20 | import grails.plugins.quartz.cleanup.JdbcCleanup 21 | import grails.plugins.quartz.listeners.ExceptionPrinterJobListener 22 | import grails.plugins.quartz.listeners.SessionBinderJobListener 23 | import groovy.util.logging.Slf4j 24 | import org.quartz.* 25 | import org.quartz.impl.matchers.GroupMatcher 26 | import org.quartz.impl.matchers.KeyMatcher 27 | import org.springframework.beans.factory.config.MethodInvokingFactoryBean 28 | import org.springframework.context.ApplicationContext 29 | import org.springframework.scheduling.quartz.SchedulerFactoryBean 30 | 31 | @Slf4j 32 | class QuartzGrailsPlugin extends Plugin { 33 | 34 | def grailsVersion = '7.0.0-SNAPSHOT > *' 35 | def watchedResources = 'file:./grails-app/jobs/**/*Job.groovy' 36 | def title = 'Quartz' 37 | def author = 'Jeff Brown' 38 | def description = 'Adds Quartz job scheduling features' 39 | def profiles = ['web'] 40 | List loadAfter = ['hibernate3', 'hibernate4', 'hibernate5', 'services'] 41 | def documentation = 'https://apache.github.io/grails-quartz/latest/' 42 | def license = 'APACHE' 43 | def issueManagement = [ system: 'Github Issues', url: 'https://github.com/apache/grails-quartz/issues' ] 44 | 45 | // Any additional developers beyond the author specified above. 46 | def developers = [ 47 | [name: 'Sergey Nebolsin', email: 'nebolsin@gmail.com'], 48 | [name: 'Graeme Rocher', email: 'graeme.rocher@gmail.com'], 49 | [name: 'Ryan Vanderwerf', email: 'rvanderwerf@gmail.com'], 50 | [name: 'Vitalii Samolovskikh', email: 'kefir@perm.ru'] 51 | ] 52 | 53 | // Online location of the plugin's browsable source code. 54 | def scm = [url: 'https://github.com/apache/grails-quartz'] 55 | 56 | Closure doWithSpring() { 57 | 58 | { -> 59 | Properties properties = loadQuartzProperties() 60 | 61 | 62 | boolean hasHibernate = hasHibernate(manager) 63 | def hasJdbcStore = properties['org.quartz.jdbcStore']?.toBoolean() 64 | if (hasJdbcStore==null) { 65 | hasJdbcStore = true 66 | } 67 | 68 | def pluginEnabled = properties['org.quartz.pluginEnabled']?.toBoolean() 69 | if (pluginEnabled==null) { 70 | pluginEnabled = true 71 | } 72 | 73 | if (pluginEnabled) { 74 | def purgeTables = properties['org.quartz.purgeQuartzTablesOnStartup']?.toBoolean() 75 | 76 | if (purgeTables==null) { 77 | purgeTables = false 78 | } 79 | 80 | if (hasJdbcStore && hasHibernate && purgeTables) { 81 | purgeTablesBean(JdbcCleanup) { bean -> 82 | dataSource = ref(properties['org.quartz.jdbcStoreDataSource'] ?: 'dataSource') 83 | bean.autowire = 'byName' 84 | } 85 | } 86 | // Configure job beans 87 | grailsApplication.jobClasses.each { GrailsJobClass jobClass -> 88 | configureJobBeans.delegate = delegate 89 | configureJobBeans(jobClass, hasHibernate) 90 | } 91 | 92 | // Configure the session listener if there is the Hibernate is configured 93 | if (hasHibernate) { 94 | log.debug("Registering hibernate SessionBinderJobListener") 95 | 96 | // register SessionBinderJobListener to bind Hibernate Session to each Job's thread 97 | "${SessionBinderJobListener.NAME}"(SessionBinderJobListener) { bean -> 98 | bean.autowire = "byName" 99 | } 100 | } 101 | 102 | // register global ExceptionPrinterJobListener which will log exceptions occured 103 | // during job's execution 104 | "${ExceptionPrinterJobListener.NAME}"(ExceptionPrinterJobListener) 105 | 106 | // Configure the job factory to create job instances on executions. 107 | quartzJobFactory(GrailsJobFactory) 108 | 109 | // Configure Scheduler 110 | configureScheduler.delegate = delegate 111 | configureScheduler() 112 | } 113 | } 114 | } 115 | 116 | /** 117 | * Configure job beans. 118 | */ 119 | def configureJobBeans = { GrailsJobClass jobClass, boolean hasHibernate = true -> 120 | def fullName = jobClass.fullName 121 | 122 | try { 123 | "${fullName}Class"(MethodInvokingFactoryBean) { 124 | targetObject = ref("grailsApplication", false) 125 | targetMethod = "getArtefact" 126 | arguments = [JobArtefactHandler.TYPE, jobClass.fullName] 127 | } 128 | 129 | "${fullName}"(ref("${fullName}Class")) { bean -> 130 | bean.factoryMethod = "newInstance" 131 | bean.autowire = "byName" 132 | bean.scope = "prototype" 133 | } 134 | } catch (Exception e) { 135 | log.error("Error declaring ${fullName}Detail bean in context", e) 136 | } 137 | } 138 | 139 | /** 140 | * Loads the quartz stanza from the grails configuration and turns it into a 141 | * flattened Properties object suitable for use by the Quartz SchedulerFactoryBean. 142 | * @return Quartz properties as defined in the Grails Configuration object 143 | */ 144 | def loadQuartzProperties() { 145 | Properties quartzProperties = new Properties() 146 | if (config.get('quartz')) { 147 | // Convert to a properties file adding a prefix to each property 148 | ConfigObject configObject = new ConfigObject() 149 | configObject.putAll(config.get('quartz') ?: [:]) 150 | quartzProperties << configObject.toProperties('org.quartz') 151 | } 152 | quartzProperties 153 | } 154 | 155 | def configureScheduler = { -> 156 | Properties properties = loadQuartzProperties() 157 | quartzScheduler(SchedulerFactoryBean) { bean -> 158 | quartzProperties = properties 159 | // The bean name is used by the factory bean as the scheduler name so you must explicitly set it if 160 | // you want a name different from the bean name. 161 | if (quartzProperties['org.quartz.scheduler.instanceName']) { 162 | schedulerName = properties['org.quartz.scheduler.instanceName'] 163 | } 164 | 165 | // delay scheduler startup to after-bootstrap stage 166 | if (quartzProperties['org.quartz.autoStartup']) { 167 | autoStartup = false // we dont want to auto startup this bean as this bean autostartup is not grails aware. 168 | } 169 | // Store 170 | def hasJdbcStore = quartzProperties['org.quartz.jdbcStore']?.toBoolean() 171 | if (hasJdbcStore == null) { 172 | hasJdbcStore = true 173 | } 174 | if (hasJdbcStore) { 175 | dataSource = ref(quartzProperties['org.quartz.jdbcStoreDataSource'] ?: 'dataSource') 176 | transactionManager = ref('transactionManager') 177 | } 178 | if (quartzProperties['org.quartz.waitForJobsToCompleteOnShutdown']) { 179 | waitForJobsToCompleteOnShutdown = quartzProperties['org.quartz.waitForJobsToCompleteOnShutdown']?.toBoolean() 180 | } 181 | if (quartzProperties['org.quartz.exposeSchedulerInRepository']) { 182 | exposeSchedulerInRepository = quartzProperties['org.quartz.exposeSchedulerInRepository']?.toBoolean() 183 | } 184 | 185 | 186 | jobFactory = quartzJobFactory 187 | 188 | // Global listeners on each job. 189 | globalJobListeners = [ref(ExceptionPrinterJobListener.NAME)] 190 | 191 | 192 | } 193 | } 194 | 195 | void onChange( Map event) { 196 | def pluginEnabled = properties['org.quartz.pluginEnabled']?.toBoolean() 197 | if (pluginEnabled==null) { 198 | pluginEnabled = true 199 | } 200 | 201 | if (pluginEnabled) { 202 | if(event.source) { 203 | boolean hasHibernate = hasHibernate(manager) 204 | def jobClass = grailsApplication.addArtefact(JobArtefactHandler.TYPE,event.source) 205 | def jobName = "${jobClass.propertyName}" 206 | beans { 207 | configureJobBeans.delegate = delegate 208 | configureJobBeans(jobClass, hasHibernate) 209 | } 210 | } 211 | refreshJobs(true) 212 | } 213 | } 214 | 215 | /** 216 | * Schedules jobs. Creates job details and trigger beans. And schedules them. 217 | */ 218 | def scheduleJob(GrailsJobClass jobClass, ApplicationContext ctx, boolean hasHibernate) { 219 | Scheduler scheduler = ctx.quartzScheduler 220 | if (scheduler) { 221 | def fullName = jobClass.fullName 222 | 223 | // Creates job details 224 | JobDetailFactoryBean jdfb = new JobDetailFactoryBean() 225 | jdfb.jobClass = jobClass 226 | jdfb.afterPropertiesSet() 227 | JobDetail jobDetail = jdfb.object 228 | 229 | // adds the job to the scheduler, and associates triggers with it 230 | scheduler.addJob(jobDetail, true) 231 | def hasJdbcStore = grailsApplication.config.getProperty('quartz.jdbcStore')?.toBoolean() 232 | if (hasJdbcStore == null) { 233 | hasJdbcStore = true 234 | } 235 | 236 | // The session listener if is needed 237 | if (hasHibernate && (jobClass.sessionRequired || hasJdbcStore)) { 238 | SessionBinderJobListener listener = ctx.getBean(SessionBinderJobListener.NAME) 239 | if (listener != null) { 240 | ListenerManager listenerManager = scheduler.getListenerManager() 241 | KeyMatcher matcher = KeyMatcher.keyEquals(jobDetail.key) 242 | if (listenerManager.getJobListener(listener.getName()) == null) { 243 | listenerManager.addJobListener(listener, matcher) 244 | } else { 245 | listenerManager.addJobListenerMatcher(listener.getName(), matcher) 246 | } 247 | } else { 248 | log.error("The SessionBinderJobListener has not been initialized.") 249 | } 250 | } 251 | 252 | // Creates and schedules triggers 253 | jobClass.triggers.each { name, Expando descriptor -> 254 | CustomTriggerFactoryBean factory = new CustomTriggerFactoryBean() 255 | factory.triggerClass = descriptor.triggerClass 256 | factory.triggerAttributes = descriptor.triggerAttributes 257 | factory.jobDetail = jobDetail 258 | factory.afterPropertiesSet() 259 | Trigger trigger = factory.object 260 | 261 | TriggerKey key = trigger.key 262 | log.debug("Scheduling $fullName with trigger $key: ${trigger}") 263 | if (scheduler.getTrigger(key) != null) { 264 | scheduler.rescheduleJob(key, trigger) 265 | } else { 266 | scheduler.scheduleJob(trigger) 267 | } 268 | log.debug("Job ${fullName} scheduled") 269 | } 270 | } else { 271 | log.error("Failed to schedule job details and job triggers: scheduler not found.") 272 | } 273 | //} 274 | } 275 | 276 | 277 | 278 | private boolean hasHibernate(manager) { 279 | manager?.hasGrailsPlugin("hibernate") || 280 | manager?.hasGrailsPlugin("hibernate3") || 281 | manager?.hasGrailsPlugin("hibernate4") || 282 | manager?.hasGrailsPlugin("hibernate5") 283 | } 284 | 285 | void refreshJobs(ignoreErrors=false) { 286 | def pluginEnabled = grailsApplication.config.getProperty('quartz.pluginEnabled')?.toBoolean() 287 | if (pluginEnabled == null) { 288 | pluginEnabled = true 289 | } 290 | if(pluginEnabled) { 291 | def quartzScheduler = applicationContext.quartzScheduler 292 | 293 | Set jobKeys = applicationContext.quartzScheduler.getJobKeys(GroupMatcher.anyGroup()) 294 | 295 | //Remove any recently removed / disabled Jobs 296 | jobKeys.each { JobKey key -> 297 | def match = grailsApplication.jobClasses.find{ GrailsJobClass jobClass -> jobClass.isEnabled() && jobClass.group == key.group && jobClass.clazz.name == key.name } 298 | if(!match) { 299 | log.info("Removing No longer Active Job: ${key.name}") 300 | def triggersForJob = quartzScheduler.getTriggersOfJob(key)?.collect{it.key} 301 | if(triggersForJob) { 302 | //clean up triggers before we remove the job 303 | quartzScheduler.unscheduleJobs(triggersForJob) 304 | } 305 | quartzScheduler.deleteJob(key) 306 | } 307 | } 308 | 309 | //Add new jobs 310 | grailsApplication.jobClasses.findAll{GrailsJobClass jobClass -> jobClass.isEnabled()}.each { GrailsJobClass jobClass -> 311 | try { 312 | scheduleJob(jobClass, applicationContext, hasHibernate(manager)) 313 | def clz = jobClass.clazz 314 | clz.scheduler = applicationContext.quartzScheduler 315 | clz.grailsJobClass = jobClass 316 | } catch(ex) { 317 | if(ignoreErrors) { 318 | log.error("Error Scheduling Job Class ${jobClass} - ${ex.message}",ex) 319 | } else { 320 | throw ex 321 | } 322 | } 323 | 324 | } 325 | } 326 | 327 | } 328 | 329 | void onStartup(Map event) { 330 | def autoStart = grailsApplication.config.getProperty('quartz.autoStartup')?.toBoolean() 331 | if (autoStart == null) { 332 | autoStart = true 333 | } 334 | def pluginEnabled = grailsApplication.config.getProperty('quartz.pluginEnabled')?.toBoolean() 335 | if (pluginEnabled == null) { 336 | pluginEnabled = true 337 | } 338 | 339 | if (pluginEnabled) { 340 | refreshJobs() 341 | if(autoStart) { 342 | applicationContext.quartzScheduler.start() 343 | log.info("Quartz Scheduler - Started") 344 | } 345 | } 346 | log.debug("Scheduled Job Classes count: " + grailsApplication.jobClasses.size()) 347 | } 348 | 349 | void onShutdown(Map event) { 350 | def pluginEnabled = grailsApplication.config.getProperty('quartz.pluginEnabled')?.toBoolean() 351 | if (pluginEnabled == null) { 352 | pluginEnabled = true 353 | } 354 | 355 | if (pluginEnabled) { 356 | Boolean waitForJobsToCompleteOnShutdown = grailsApplication.config.getProperty('quartz.waitForJobsToCompleteOnShutdown')?.toBoolean() 357 | if (waitForJobsToCompleteOnShutdown == null) { 358 | waitForJobsToCompleteOnShutdown = true 359 | } 360 | applicationContext.quartzScheduler.shutdown(waitForJobsToCompleteOnShutdown) 361 | } 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugins/quartz/CustomTriggerFactoryBeanSpec.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.quartz 2 | 3 | import grails.plugins.quartz.config.TriggersConfigBuilder 4 | 5 | import org.quartz.CronTrigger 6 | import org.quartz.DailyTimeIntervalTrigger 7 | import org.quartz.DateBuilder 8 | import org.quartz.SimpleTrigger 9 | import org.quartz.TimeOfDay 10 | import org.quartz.Trigger 11 | import org.quartz.impl.triggers.DailyTimeIntervalTriggerImpl 12 | import spock.lang.Specification 13 | 14 | /** 15 | * Tests for CustomTriggerFactoryBean 16 | * 17 | * @author Vitalii Samolovskikh aka Kefir 18 | */ 19 | class CustomTriggerFactoryBeanSpec extends Specification { 20 | 21 | private static final String CRON_EXPRESSION = '0 15 6 * * ?' 22 | private static final TimeOfDay START_TIME = new TimeOfDay(10, 0) 23 | private static final TimeOfDay END_TIME = new TimeOfDay(11, 30) 24 | 25 | void 'testFactory'() { 26 | setup: 27 | def builder = new TriggersConfigBuilder('TestJob', null) 28 | def closure = { 29 | simple name: 'simple', group: 'group', startDelay: 500, repeatInterval: 1000, repeatCount: 3 30 | cron name: 'cron', group: 'group', cronExpression: CRON_EXPRESSION 31 | custom name: 'custom', group: 'group', triggerClass: DailyTimeIntervalTriggerImpl, 32 | startTimeOfDay: START_TIME, endTimeOfDay: END_TIME, 33 | repeatIntervalUnit: DateBuilder.IntervalUnit.MINUTE, repeatInterval: 5 34 | } 35 | builder.build(closure) 36 | 37 | Map triggers = [:] 38 | 39 | builder.triggers.values().each { 40 | CustomTriggerFactoryBean factory = new CustomTriggerFactoryBean() 41 | factory.setTriggerClass(it.triggerClass) 42 | factory.setTriggerAttributes(it.triggerAttributes) 43 | factory.afterPropertiesSet() 44 | Trigger trigger = factory.getObject() as Trigger 45 | triggers.put(trigger.key.name, trigger) 46 | } 47 | 48 | expect: 49 | assert triggers['simple'] instanceof SimpleTrigger 50 | SimpleTrigger simpleTrigger = triggers['simple'] as SimpleTrigger 51 | assert 'simple' == simpleTrigger.key.name 52 | assert 'group' == simpleTrigger.key.group 53 | assert 1000 == simpleTrigger.repeatInterval 54 | assert 3 == simpleTrigger.repeatCount 55 | 56 | assert triggers['cron'] instanceof CronTrigger 57 | CronTrigger cronTrigger = triggers['cron'] as CronTrigger 58 | assert 'cron' == cronTrigger.key.name 59 | assert 'group' == cronTrigger.key.group 60 | assert CRON_EXPRESSION == cronTrigger.getCronExpression() 61 | 62 | assert triggers['custom'] instanceof DailyTimeIntervalTrigger 63 | DailyTimeIntervalTrigger customTrigger = triggers['custom'] as DailyTimeIntervalTrigger 64 | assert 'custom' == customTrigger.key.name 65 | assert 'group' == customTrigger.key.group 66 | assert START_TIME == customTrigger.startTimeOfDay 67 | assert END_TIME == customTrigger.endTimeOfDay 68 | assert DateBuilder.IntervalUnit.MINUTE == customTrigger.repeatIntervalUnit 69 | assert 5 == customTrigger.repeatInterval 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugins/quartz/JobDescriptorSpec.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.quartz 2 | 3 | import org.quartz.* 4 | import org.quartz.impl.StdSchedulerFactory 5 | import spock.lang.Specification 6 | 7 | /** 8 | * Unit tests for JobDescriptor. 9 | * 10 | * @author Vitalii Samolovskikh aka Kefir 11 | */ 12 | class JobDescriptorSpec extends Specification { 13 | 14 | private Scheduler scheduler 15 | private JobDetail job 16 | private Trigger trigger 17 | 18 | 19 | def setup() { 20 | scheduler = StdSchedulerFactory.getDefaultScheduler() 21 | scheduler.start() 22 | job = JobBuilder.newJob(TestQuartzJob).withIdentity(new JobKey("job", "group")).build() 23 | trigger = TriggerBuilder.newTrigger() 24 | .withIdentity(new TriggerKey("trigger", "group")) 25 | .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInMinutes(2).repeatForever()) 26 | .startNow() 27 | .build() 28 | scheduler.scheduleJob(job, trigger) 29 | } 30 | 31 | def cleanup() { 32 | scheduler.shutdown() 33 | } 34 | 35 | void 'JobDescriptor builds correctly from a job and scheduler'() { 36 | when: 37 | JobDescriptor descriptor = JobDescriptor.build(job, scheduler) 38 | then: 39 | descriptor.name == 'job' 40 | descriptor.group == 'group' 41 | descriptor.triggerDescriptors.size() == 1 42 | descriptor.triggerDescriptors[0].name == 'trigger' 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugins/quartz/JobDetailFactoryBeanSpec.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.quartz 2 | 3 | import grails.core.GrailsApplication 4 | import org.quartz.JobDetail 5 | import org.quartz.JobKey 6 | import org.springframework.beans.BeanWrapper 7 | import spock.lang.Specification 8 | 9 | /** 10 | * Tests for the JobDetailFactoryBean 11 | * 12 | * @author Vitalii Samolovskikh aka Kefir 13 | */ 14 | class JobDetailFactoryBeanSpec extends Specification { 15 | 16 | private static final String JOB_NAME = 'jobName' 17 | private static final String JOB_GROUP = 'jobGroup' 18 | private static final String JOB_DESCRIPTION = 'The job description' 19 | JobDetailFactoryBean factory = new JobDetailFactoryBean() 20 | 21 | 22 | void 'testFactory1'() { 23 | setup: 24 | factory.jobClass = new GrailsJobClassMock( 25 | [ 26 | fullName : JOB_NAME, 27 | group : JOB_GROUP, 28 | concurrent : true, 29 | durability : true, 30 | sessionRequired : true, 31 | requestsRecovery: true, 32 | description : JOB_DESCRIPTION 33 | ] 34 | ) 35 | factory.afterPropertiesSet() 36 | when: 37 | JobDetail jobDetail = factory.object 38 | then: 39 | new JobKey(JOB_NAME, JOB_GROUP) == jobDetail.key 40 | JOB_NAME == jobDetail.getJobDataMap().get(JobDetailFactoryBean.JOB_NAME_PARAMETER) 41 | jobDetail.durable 42 | !jobDetail.isConcurrentExecutionDisallowed() 43 | !jobDetail.persistJobDataAfterExecution 44 | jobDetail.requestsRecovery() 45 | JOB_DESCRIPTION == jobDetail.description 46 | } 47 | 48 | void 'testFactory2'() { 49 | setup: 50 | factory.jobClass = new GrailsJobClassMock( 51 | [ 52 | fullName : JOB_NAME, 53 | group : JOB_GROUP, 54 | concurrent : false, 55 | durability : false, 56 | sessionRequired : false, 57 | requestsRecovery: false 58 | ] 59 | ) 60 | factory.afterPropertiesSet() 61 | when: 62 | JobDetail jobDetail = factory.object 63 | then: 64 | new JobKey(JOB_NAME, JOB_GROUP) == jobDetail.key 65 | JOB_NAME == jobDetail.getJobDataMap().get(JobDetailFactoryBean.JOB_NAME_PARAMETER) 66 | !jobDetail.durable 67 | jobDetail.isConcurrentExecutionDisallowed() 68 | jobDetail.persistJobDataAfterExecution 69 | !jobDetail.requestsRecovery() 70 | jobDetail.description == null 71 | } 72 | } 73 | 74 | class GrailsJobClassMock implements GrailsJobClass { 75 | String group 76 | String fullName 77 | boolean concurrent 78 | boolean jobEnabled 79 | boolean durability 80 | boolean sessionRequired 81 | boolean requestsRecovery 82 | String description 83 | 84 | void execute() {} 85 | Map getTriggers() {} 86 | boolean byName() { false } 87 | boolean byType() { false } 88 | boolean getAvailable() { false } 89 | boolean isAbstract() { false } 90 | boolean isEnabled() { true } 91 | GrailsApplication getGrailsApplication() {} 92 | 93 | @Override 94 | grails.core.GrailsApplication getApplication() { 95 | return null 96 | } 97 | 98 | Object getPropertyValue(String name) {} 99 | boolean hasProperty(String name) { false } 100 | Object newInstance() {} 101 | String getName() {} 102 | String getShortName() {} 103 | String getPropertyName() {} 104 | String getLogicalPropertyName() {} 105 | String getNaturalName() {} 106 | String getPackageName() {} 107 | Class getClazz() {} 108 | BeanWrapper getReference() {} 109 | Object getReferenceInstance() {} 110 | def T getPropertyValue(String name, Class type) {} 111 | 112 | @Override 113 | String getPluginName() { 114 | return null 115 | } 116 | 117 | void setGrailsApplication(GrailsApplication grailsApplication) {} 118 | } 119 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugins/quartz/QuartzJobTraitInjectorSpec.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.quartz 2 | 3 | import grails.artefact.Artefact 4 | import spock.lang.Specification 5 | 6 | class QuartzJobTraitInjectorSpec extends Specification { 7 | 8 | void 'test that the job trait is applied'() { 9 | expect: 10 | QuartzJob.isAssignableFrom TraitTestJob 11 | } 12 | } 13 | 14 | @Artefact('Job') 15 | class TraitTestJob {} 16 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugins/quartz/TestQuartzJob.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.quartz 2 | 3 | import org.quartz.Job 4 | import org.quartz.JobExecutionContext 5 | import org.quartz.JobExecutionException 6 | 7 | /** 8 | * @author Vitalii Samolovskikh aka Kefir 9 | */ 10 | class TestQuartzJob implements Job { 11 | @Override 12 | void execute(JobExecutionContext context) throws JobExecutionException {} 13 | } 14 | -------------------------------------------------------------------------------- /src/test/groovy/grails/plugins/quartz/TriggerDescriptorSpec.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.quartz 2 | 3 | import org.quartz.JobBuilder 4 | import org.quartz.JobDetail 5 | import org.quartz.JobKey 6 | import org.quartz.Scheduler 7 | import org.quartz.SimpleScheduleBuilder 8 | import org.quartz.Trigger 9 | import org.quartz.TriggerBuilder 10 | import org.quartz.TriggerKey 11 | import org.quartz.impl.StdSchedulerFactory 12 | import spock.lang.Specification 13 | 14 | /** 15 | * Unit tests for TriggerDescriptor 16 | * 17 | * @author Vitalii Samolovskikh aka Kefir 18 | */ 19 | class TriggerDescriptorSpec extends Specification { 20 | 21 | private Scheduler scheduler 22 | private JobDetail job 23 | private Trigger trigger 24 | 25 | def setup() { 26 | scheduler = StdSchedulerFactory.getDefaultScheduler() 27 | 28 | scheduler.start() 29 | 30 | job = JobBuilder.newJob(TestQuartzJob).withIdentity(new JobKey("job", "group")).build() 31 | trigger = TriggerBuilder.newTrigger() 32 | .withIdentity(new TriggerKey("trigger", "group")) 33 | .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInMinutes(2).repeatForever()) 34 | .startNow() 35 | .build() 36 | 37 | scheduler.scheduleJob(job, trigger) 38 | } 39 | 40 | def cleanup() { 41 | scheduler.shutdown() 42 | } 43 | 44 | 45 | void 'build TriggerDescriptor correctly'() { 46 | when: 47 | TriggerDescriptor descriptor = 48 | TriggerDescriptor.build(JobDescriptor.build(job, scheduler), trigger, scheduler) 49 | then: 50 | descriptor.name == 'trigger' 51 | descriptor.group == 'group' 52 | descriptor.state == Trigger.TriggerState.NORMAL 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------