├── .github ├── release-drafter.yml ├── renovate.json └── workflows │ ├── gradle.yml │ ├── release-notes.yml │ └── release.yml ├── .gitignore ├── .sdkmanrc ├── LICENSE ├── README.md ├── build.gradle ├── buildSrc └── build.gradle ├── examples └── redis-demo │ ├── build.gradle │ ├── grails-app │ ├── assets │ │ ├── images │ │ │ ├── apple-touch-icon-retina.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon.ico │ │ │ ├── grails_logo.png │ │ │ ├── skin │ │ │ │ ├── database_add.png │ │ │ │ ├── database_delete.png │ │ │ │ ├── database_edit.png │ │ │ │ ├── database_save.png │ │ │ │ ├── database_table.png │ │ │ │ ├── exclamation.png │ │ │ │ ├── house.png │ │ │ │ ├── information.png │ │ │ │ ├── shadow.jpg │ │ │ │ ├── sorted_asc.gif │ │ │ │ └── sorted_desc.gif │ │ │ ├── spinner.gif │ │ │ └── springsource.png │ │ ├── javascripts │ │ │ ├── application.js │ │ │ └── jquery.js │ │ └── stylesheets │ │ │ ├── application.css │ │ │ ├── errors.css │ │ │ ├── main.css │ │ │ └── mobile.css │ ├── conf │ │ ├── application.yml │ │ ├── logback-spring.xml │ │ └── spring │ │ │ └── resources.groovy │ ├── controllers │ │ └── com │ │ │ └── example │ │ │ ├── IndexController.groovy │ │ │ └── UrlMappings.groovy │ ├── domain │ │ └── com │ │ │ └── example │ │ │ └── Book.groovy │ ├── init │ │ └── com │ │ │ └── example │ │ │ ├── Application.groovy │ │ │ └── BootStrap.groovy │ ├── services │ │ └── com │ │ │ └── example │ │ │ ├── BookCreateService.groovy │ │ │ └── BookService.groovy │ └── views │ │ ├── error.gsp │ │ ├── index.gsp │ │ └── layouts │ │ └── main.gsp │ └── src │ └── test │ └── groovy │ └── com │ └── example │ ├── HoldersIntegrationSpec.groovy │ ├── MemoizeAnnotationSpec.groovy │ ├── MemoizeObjectAnnotationSpec.groovy │ ├── ProxyAwareSpec.groovy │ ├── RedisIntegrationSpec.groovy │ ├── RedisMemoizeDomainSpec.groovy │ ├── RedisMemoizeServiceSpec.groovy │ └── RedisMultipleConnectionsConfigSpec.groovy ├── gradle.properties ├── gradle ├── docs-config.gradle ├── examples-config.gradle ├── java-config.gradle ├── publish-config.gradle ├── test-config.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── plugin ├── build.gradle ├── grails-app │ ├── conf │ │ ├── application.yml │ │ └── logback-spring.xml │ ├── init │ │ └── redis │ │ │ └── Application.groovy │ └── taglib │ │ └── grails │ │ └── plugins │ │ └── redis │ │ └── RedisTagLib.groovy └── src │ ├── main │ └── groovy │ │ ├── grails │ │ └── plugins │ │ │ └── redis │ │ │ ├── Memoize.groovy │ │ │ ├── MemoizeDomainList.groovy │ │ │ ├── MemoizeDomainObject.groovy │ │ │ ├── MemoizeHash.groovy │ │ │ ├── MemoizeHashField.groovy │ │ │ ├── MemoizeList.groovy │ │ │ ├── MemoizeObject.groovy │ │ │ ├── MemoizeScore.groovy │ │ │ ├── RedisService.groovy │ │ │ ├── ast │ │ │ ├── AbstractMemoizeASTTransformation.groovy │ │ │ ├── MemoizeASTTransformation.groovy │ │ │ ├── MemoizeDomainListASTTransformation.groovy │ │ │ ├── MemoizeDomainObjectASTTransformation.groovy │ │ │ ├── MemoizeHashASTTransformation.groovy │ │ │ ├── MemoizeHashFieldASTTransformation.groovy │ │ │ ├── MemoizeListASTTransformation.groovy │ │ │ ├── MemoizeObjectASTTransformation.groovy │ │ │ └── MemoizeScoreASTTransformation.groovy │ │ │ └── util │ │ │ └── RedisConfigurationUtil.groovy │ │ └── redis │ │ └── RedisGrailsPlugin.groovy │ └── test │ └── groovy │ └── grails │ └── plugins │ └── redis │ ├── RedisServiceSpec.groovy │ └── RedisTagLibSpec.groovy └── settings.gradle /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: $RESOLVED_VERSION 2 | tag-template: v$RESOLVED_VERSION 3 | categories: 4 | - title: 🚀 Features 5 | labels: 6 | - "type: enhancement" 7 | - "type: new feature" 8 | - "type: major" 9 | - title: 🚀 Bug Fixes/Improvements 10 | labels: 11 | - "type: improvement" 12 | - "type: bug" 13 | - "type: minor" 14 | - title: 🛠 Dependency upgrades 15 | labels: 16 | - "type: dependency upgrade" 17 | - "dependencies" 18 | - title: ⚙️ Build/CI 19 | labels: 20 | - "type: ci" 21 | - "type: build" 22 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 23 | version-resolver: 24 | major: 25 | labels: 26 | - 'type: major' 27 | minor: 28 | labels: 29 | - 'type: minor' 30 | patch: 31 | labels: 32 | - 'type: patch' 33 | default: patch 34 | template: | 35 | ## What's Changed 36 | $CHANGES 37 | ## Contributors 38 | $CONTRIBUTORS -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.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 | build: 14 | name: "Build (Redis ${{ matrix.redis-version }})" 15 | runs-on: ubuntu-24.04 16 | strategy: 17 | matrix: 18 | redis-version: [6, 7] 19 | services: 20 | redis: 21 | image: redis:${{ matrix.redis-version }} 22 | ports: 23 | - 6379:6379 24 | options: >- 25 | --health-cmd "redis-cli ping" 26 | --health-interval 10s 27 | --health-timeout 5s 28 | --health-retries 5 29 | steps: 30 | - name: "📥 Checkout repository" 31 | uses: actions/checkout@v4 32 | - name: "☕️ Setup JDK" 33 | uses: actions/setup-java@v4 34 | with: 35 | java-version: 17 36 | distribution: liberica 37 | - name: "🐘 Setup Gradle" 38 | uses: gradle/actions/setup-gradle@v4 39 | with: 40 | develocity-access-key: ${{ secrets.GRAILS_DEVELOCITY_ACCESS_KEY }} 41 | - name: "🏃‍♂️ Run Build" 42 | id: build 43 | env: 44 | REDIS_HOST: redis 45 | REDIS_PORT: 6379 46 | run: ./gradlew build --continue 47 | publish_snapshot: 48 | if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' 49 | needs: build 50 | runs-on: ubuntu-24.04 51 | permissions: 52 | contents: write 53 | steps: 54 | - name: "📥 Checkout repository" 55 | uses: actions/checkout@v4 56 | - name: "☕️ Setup JDK" 57 | uses: actions/setup-java@v4 58 | with: 59 | java-version: 17 60 | distribution: liberica 61 | - name: "🐘 Setup Gradle" 62 | uses: gradle/actions/setup-gradle@v4 63 | with: 64 | develocity-access-key: ${{ secrets.GRAILS_DEVELOCITY_ACCESS_KEY }} 65 | - name: "📤 Publish Snapshot Artifacts" 66 | id: publish 67 | env: 68 | GITHUB_MAVEN_PASSWORD: ${{ secrets.GITHUB_TOKEN }} 69 | GRAILS_PUBLISH_RELEASE: 'false' 70 | MAVEN_PUBLISH_USERNAME: ${{ secrets.NEXUS_USER }} 71 | MAVEN_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PW }} 72 | MAVEN_PUBLISH_URL: ${{ secrets.GRAILS_NEXUS_PUBLISH_SNAPSHOT_URL }} 73 | run: ./gradlew --no-build-cache publish 74 | - name: "🔨 Generate Snapshot Documentation" 75 | run: ./gradlew :grails-redis:groovydoc 76 | - name: "🚀 Publish to Github Pages" 77 | uses: apache/grails-github-actions/deploy-github-pages@asf 78 | env: 79 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | GRADLE_PUBLISH_RELEASE: 'false' 81 | SOURCE_FOLDER: plugin/build/docs 82 | -------------------------------------------------------------------------------- /.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 | steps: 12 | - name: "📥 Checkout repository" 13 | uses: actions/checkout@v4 14 | - name: "☕️ Setup JDK" 15 | uses: actions/setup-java@v4 16 | with: 17 | java-version: 17 18 | distribution: liberica 19 | - name: "🐘 Setup Gradle" 20 | uses: gradle/actions/setup-gradle@v4 21 | with: 22 | develocity-access-key: ${{ secrets.GRAILS_DEVELOCITY_ACCESS_KEY }} 23 | - name: "📝 Store the current release version" 24 | id: release_version 25 | run: echo "release_version=${GITHUB_REF:11}" >> $GITHUB_OUTPUT 26 | - name: "⚙️ Run pre-release" 27 | uses: apache/grails-github-actions/pre-release@asf 28 | - name: "🔐 Generate key file for artifact signing" 29 | env: 30 | SECRING_FILE: ${{ secrets.SECRING_FILE }} 31 | run: echo $SECRING_FILE | base64 -d > ${{ github.workspace }}/secring.gpg 32 | - name: "📤 Publish to Sonatype - close and release staging repository" 33 | env: 34 | GITHUB_MAVEN_PASSWORD: ${{ secrets.GITHUB_TOKEN }} 35 | GRAILS_PUBLISH_RELEASE: 'true' 36 | NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_USER }} 37 | NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PW }} 38 | NEXUS_PUBLISH_URL: ${{ secrets.GRAILS_NEXUS_PUBLISH_RELEASE_URL }} 39 | NEXUS_PUBLISH_STAGING_PROFILE_ID: ${{ secrets.NEXUS_PUBLISH_STAGING_PROFILE_ID }} # TODO: What about this secret? 40 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 41 | SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} 42 | run: > 43 | ./gradlew --refresh-dependencies 44 | -Psigning.secretKeyRingFile=${{ github.workspace }}/secring.gpg 45 | publishToSonatype 46 | closeAndReleaseSonatypeStagingRepository 47 | - name: "🔨 Build Documentation" 48 | run: ./gradlew :grails-redis:groovydoc 49 | - name: "🚀 Publish to Github Pages" 50 | uses: apache/grails-github-actions/deploy-github-pages@asf 51 | env: 52 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | GRADLE_PUBLISH_RELEASE: 'true' 54 | SOURCE_FOLDER: plugin/build/docs 55 | VERSION: ${{ steps.release_version.outputs.release_version }} 56 | - name: "⚙️ Run post-release" 57 | uses: apache/grails-github-actions/post-release@asf 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .settings 2 | *.log 3 | *.un~ 4 | .DS_Store 5 | bin 6 | build 7 | .gradle 8 | .project 9 | .classpath 10 | .idea 11 | *.iml 12 | *.ipr 13 | *.iws 14 | *.swp 15 | out 16 | -------------------------------------------------------------------------------- /.sdkmanrc: -------------------------------------------------------------------------------- 1 | # Enable auto-env through the sdkman_auto_env config - https://sdkman.io/usage#env 2 | java=17.0.14-librca -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * Apache License 3 | * Version 2.0, January 2004 4 | * http://www.apache.org/licenses/ 5 | * 6 | * TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | * 8 | * 1. Definitions. 9 | * 10 | * "License" shall mean the terms and conditions for use, reproduction, 11 | * and distribution as defined by Sections 1 through 9 of this document. 12 | * 13 | * "Licensor" shall mean the copyright owner or entity authorized by 14 | * the copyright owner that is granting the License. 15 | * 16 | * "Legal Entity" shall mean the union of the acting entity and all 17 | * other entities that control, are controlled by, or are under common 18 | * control with that entity. For the purposes of this definition, 19 | * "control" means (i) the power, direct or indirect, to cause the 20 | * direction or management of such entity, whether by contract or 21 | * otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | * outstanding shares, or (iii) beneficial ownership of such entity. 23 | * 24 | * "You" (or "Your") shall mean an individual or Legal Entity 25 | * exercising permissions granted by this License. 26 | * 27 | * "Source" form shall mean the preferred form for making modifications, 28 | * including but not limited to software source code, documentation 29 | * source, and configuration files. 30 | * 31 | * "Object" form shall mean any form resulting from mechanical 32 | * transformation or translation of a Source form, including but 33 | * not limited to compiled object code, generated documentation, 34 | * and conversions to other media types. 35 | * 36 | * "Work" shall mean the work of authorship, whether in Source or 37 | * Object form, made available under the License, as indicated by a 38 | * copyright notice that is included in or attached to the work 39 | * (an example is provided in the Appendix below). 40 | * 41 | * "Derivative Works" shall mean any work, whether in Source or Object 42 | * form, that is based on (or derived from) the Work and for which the 43 | * editorial revisions, annotations, elaborations, or other modifications 44 | * represent, as a whole, an original work of authorship. For the purposes 45 | * of this License, Derivative Works shall not include works that remain 46 | * separable from, or merely link (or bind by name) to the interfaces of, 47 | * the Work and Derivative Works thereof. 48 | * 49 | * "Contribution" shall mean any work of authorship, including 50 | * the original version of the Work and any modifications or additions 51 | * to that Work or Derivative Works thereof, that is intentionally 52 | * submitted to Licensor for inclusion in the Work by the copyright owner 53 | * or by an individual or Legal Entity authorized to submit on behalf of 54 | * the copyright owner. For the purposes of this definition, "submitted" 55 | * means any form of electronic, verbal, or written communication sent 56 | * to the Licensor or its representatives, including but not limited to 57 | * communication on electronic mailing lists, source code control systems, 58 | * and issue tracking systems that are managed by, or on behalf of, the 59 | * Licensor for the purpose of discussing and improving the Work, but 60 | * excluding communication that is conspicuously marked or otherwise 61 | * designated in writing by the copyright owner as "Not a Contribution." 62 | * 63 | * "Contributor" shall mean Licensor and any individual or Legal Entity 64 | * on behalf of whom a Contribution has been received by Licensor and 65 | * subsequently incorporated within the Work. 66 | * 67 | * 2. Grant of Copyright License. Subject to the terms and conditions of 68 | * this License, each Contributor hereby grants to You a perpetual, 69 | * worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | * copyright license to reproduce, prepare Derivative Works of, 71 | * publicly display, publicly perform, sublicense, and distribute the 72 | * Work and such Derivative Works in Source or Object form. 73 | * 74 | * 3. Grant of Patent License. Subject to the terms and conditions of 75 | * this License, each Contributor hereby grants to You a perpetual, 76 | * worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | * (except as stated in this section) patent license to make, have made, 78 | * use, offer to sell, sell, import, and otherwise transfer the Work, 79 | * where such license applies only to those patent claims licensable 80 | * by such Contributor that are necessarily infringed by their 81 | * Contribution(s) alone or by combination of their Contribution(s) 82 | * with the Work to which such Contribution(s) was submitted. If You 83 | * institute patent litigation against any entity (including a 84 | * cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | * or a Contribution incorporated within the Work constitutes direct 86 | * or contributory patent infringement, then any patent licenses 87 | * granted to You under this License for that Work shall terminate 88 | * as of the date such litigation is filed. 89 | * 90 | * 4. Redistribution. You may reproduce and distribute copies of the 91 | * Work or Derivative Works thereof in any medium, with or without 92 | * modifications, and in Source or Object form, provided that You 93 | * meet the following conditions: 94 | * 95 | * (a) You must give any other recipients of the Work or 96 | * Derivative Works a copy of this License; and 97 | * 98 | * (b) You must cause any modified files to carry prominent notices 99 | * stating that You changed the files; and 100 | * 101 | * (c) You must retain, in the Source form of any Derivative Works 102 | * that You distribute, all copyright, patent, trademark, and 103 | * attribution notices from the Source form of the Work, 104 | * excluding those notices that do not pertain to any part of 105 | * the Derivative Works; and 106 | * 107 | * (d) If the Work includes a "NOTICE" text file as part of its 108 | * distribution, then any Derivative Works that You distribute must 109 | * include a readable copy of the attribution notices contained 110 | * within such NOTICE file, excluding those notices that do not 111 | * pertain to any part of the Derivative Works, in at least one 112 | * of the following places: within a NOTICE text file distributed 113 | * as part of the Derivative Works; within the Source form or 114 | * documentation, if provided along with the Derivative Works; or, 115 | * within a display generated by the Derivative Works, if and 116 | * wherever such third-party notices normally appear. The contents 117 | * of the NOTICE file are for informational purposes only and 118 | * do not modify the License. You may add Your own attribution 119 | * notices within Derivative Works that You distribute, alongside 120 | * or as an addendum to the NOTICE text from the Work, provided 121 | * that such additional attribution notices cannot be construed 122 | * as modifying the License. 123 | * 124 | * You may add Your own copyright statement to Your modifications and 125 | * may provide additional or different license terms and conditions 126 | * for use, reproduction, or distribution of Your modifications, or 127 | * for any such Derivative Works as a whole, provided Your use, 128 | * reproduction, and distribution of the Work otherwise complies with 129 | * the conditions stated in this License. 130 | * 131 | * 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | * any Contribution intentionally submitted for inclusion in the Work 133 | * by You to the Licensor shall be under the terms and conditions of 134 | * this License, without any additional terms or conditions. 135 | * Notwithstanding the above, nothing herein shall supersede or modify 136 | * the terms of any separate license agreement you may have executed 137 | * with Licensor regarding such Contributions. 138 | * 139 | * 6. Trademarks. This License does not grant permission to use the trade 140 | * names, trademarks, service marks, or product names of the Licensor, 141 | * except as required for reasonable and customary use in describing the 142 | * origin of the Work and reproducing the content of the NOTICE file. 143 | * 144 | * 7. Disclaimer of Warranty. Unless required by applicable law or 145 | * agreed to in writing, Licensor provides the Work (and each 146 | * Contributor provides its Contributions) on an "AS IS" BASIS, 147 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | * implied, including, without limitation, any warranties or conditions 149 | * of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | * PARTICULAR PURPOSE. You are solely responsible for determining the 151 | * appropriateness of using or redistributing the Work and assume any 152 | * risks associated with Your exercise of permissions under this License. 153 | * 154 | * 8. Limitation of Liability. In no event and under no legal theory, 155 | * whether in tort (including negligence), contract, or otherwise, 156 | * unless required by applicable law (such as deliberate and grossly 157 | * negligent acts) or agreed to in writing, shall any Contributor be 158 | * liable to You for damages, including any direct, indirect, special, 159 | * incidental, or consequential damages of any character arising as a 160 | * result of this License or out of the use or inability to use the 161 | * Work (including but not limited to damages for loss of goodwill, 162 | * work stoppage, computer failure or malfunction, or any and all 163 | * other commercial damages or losses), even if such Contributor 164 | * has been advised of the possibility of such damages. 165 | * 166 | * 9. Accepting Warranty or Additional Liability. While redistributing 167 | * the Work or Derivative Works thereof, You may choose to offer, 168 | * and charge a fee for, acceptance of support, warranty, indemnity, 169 | * or other liability obligations and/or rights consistent with this 170 | * License. However, in accepting such obligations, You may act only 171 | * on Your own behalf and on Your sole responsibility, not on behalf 172 | * of any other Contributor, and only if You agree to indemnify, 173 | * defend, and hold each Contributor harmless for any liability 174 | * incurred by, or claims asserted against, such Contributor by reason 175 | * of your accepting any such warranty or additional liability. 176 | * 177 | * END OF TERMS AND CONDITIONS 178 | * 179 | * APPENDIX: How to apply the Apache License to your work. 180 | * 181 | * To apply the Apache License to your work, attach the following 182 | * boilerplate notice, with the fields enclosed by brackets "[]" 183 | * replaced with your own identifying information. (Don't include 184 | * the brackets!) The text should be enclosed in the appropriate 185 | * comment syntax for the file format. We also recommend that a 186 | * file or class name and description of purpose be included on the 187 | * same "printed page" as the copyright notice for easier 188 | * identification within third-party archives. 189 | * 190 | * Copyright [yyyy] [name of copyright owner] 191 | * 192 | * Licensed under the Apache License, Version 2.0 (the "License"); 193 | * you may not use this file except in compliance with the License. 194 | * You may obtain a copy of the License at 195 | * 196 | * http://www.apache.org/licenses/LICENSE-2.0 197 | * 198 | * Unless required by applicable law or agreed to in writing, software 199 | * distributed under the License is distributed on an "AS IS" BASIS, 200 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | * See the License for the specific language governing permissions and 202 | * limitations under the License. 203 | */ 204 | 205 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Workaround needed for nexus publishing bug 2 | // version and group must be specified in the root project 3 | // https://github.com/gradle-nexus/publish-plugin/issues/310 4 | version = projectVersion 5 | group = 'this.will.be.overridden' 6 | 7 | subprojects { 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 | if (name == 'grails-redis') { 16 | // This has to be applied here 17 | apply plugin: 'org.apache.grails.gradle.grails-publish' 18 | } 19 | } -------------------------------------------------------------------------------- /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 | implementation 'com.bertramlabs.plugins:asset-pipeline-gradle' 20 | } -------------------------------------------------------------------------------- /examples/redis-demo/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.bertramlabs.asset-pipeline' 3 | id 'org.apache.grails.gradle.grails-web' 4 | id 'org.apache.grails.gradle.grails-gsp' 5 | } 6 | 7 | version = projectVersion 8 | group = 'com.example' 9 | 10 | dependencies { 11 | 12 | implementation platform("org.apache.grails:grails-bom:$grailsVersion") 13 | 14 | implementation project(':grails-redis') 15 | implementation 'org.apache.grails:grails-core' 16 | implementation 'org.apache.grails:grails-data-hibernate5' 17 | implementation 'org.apache.grails:grails-databinding' 18 | implementation 'org.apache.grails:grails-domain-class' 19 | implementation 'org.apache.grails:grails-gsp' 20 | implementation 'org.apache.grails:grails-i18n' 21 | implementation 'org.apache.grails:grails-services' 22 | implementation 'org.apache.grails:grails-url-mappings' 23 | implementation 'org.apache.grails.web:grails-web-boot' 24 | 25 | runtimeOnly 'com.bertramlabs.plugins:asset-pipeline-grails' 26 | runtimeOnly 'com.h2database:h2' 27 | runtimeOnly 'org.apache.tomcat:tomcat-jdbc' 28 | runtimeOnly 'org.fusesource.jansi:jansi' 29 | runtimeOnly 'org.springframework.boot:spring-boot-autoconfigure' 30 | runtimeOnly 'org.springframework.boot:spring-boot-starter' 31 | runtimeOnly 'org.springframework.boot:spring-boot-starter-logging' 32 | runtimeOnly 'org.springframework.boot:spring-boot-starter-tomcat' 33 | 34 | testImplementation 'org.apache.grails:grails-testing-support-datamapping' 35 | testImplementation 'org.apache.grails:grails-testing-support-web' 36 | testImplementation 'org.spockframework:spock-core' 37 | } 38 | 39 | apply { 40 | from rootProject.layout.projectDirectory.file('gradle/java-config.gradle') 41 | from rootProject.layout.projectDirectory.file('gradle/test-config.gradle') 42 | from rootProject.layout.projectDirectory.file('gradle/examples-config.gradle') 43 | } -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/apple-touch-icon-retina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/apple-touch-icon-retina.png -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/apple-touch-icon.png -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/favicon.ico -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/grails_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/grails_logo.png -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/skin/database_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/skin/database_add.png -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/skin/database_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/skin/database_delete.png -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/skin/database_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/skin/database_edit.png -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/skin/database_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/skin/database_save.png -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/skin/database_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/skin/database_table.png -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/skin/exclamation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/skin/exclamation.png -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/skin/house.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/skin/house.png -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/skin/information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/skin/information.png -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/skin/shadow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/skin/shadow.jpg -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/skin/sorted_asc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/skin/sorted_asc.gif -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/skin/sorted_desc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/skin/sorted_desc.gif -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/spinner.gif -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/images/springsource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/examples/redis-demo/grails-app/assets/images/springsource.png -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js. 2 | // 3 | // Any JavaScript file within this directory can be referenced here using a relative path. 4 | // 5 | // You're free to add application-wide JavaScript to this file, but it's generally better 6 | // to create separate JavaScript files as needed. 7 | // 8 | //= require jquery 9 | //= require_tree . 10 | //= require_self 11 | 12 | if (typeof jQuery !== 'undefined') { 13 | (function($) { 14 | $('#spinner').ajaxStart(function() { 15 | $(this).fadeIn(); 16 | }).ajaxStop(function() { 17 | $(this).fadeOut(); 18 | }); 19 | })(jQuery); 20 | } 21 | -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS file within this directory can be referenced here using a relative path. 6 | * 7 | * You're free to add application-wide styles to this file and they'll appear at the top of the 8 | * compiled file, but it's generally better to create a new file per style scope. 9 | * 10 | *= require main 11 | *= require mobile 12 | *= require_self 13 | */ 14 | -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/stylesheets/errors.css: -------------------------------------------------------------------------------- 1 | h1, h2 { 2 | margin: 10px 25px 5px; 3 | } 4 | 5 | h2 { 6 | font-size: 1.1em; 7 | } 8 | 9 | .filename { 10 | font-style: italic; 11 | } 12 | 13 | .exceptionMessage { 14 | margin: 10px; 15 | border: 1px solid #000; 16 | padding: 5px; 17 | background-color: #E9E9E9; 18 | } 19 | 20 | .stack, 21 | .snippet { 22 | margin: 0 25px 10px; 23 | } 24 | 25 | .stack, 26 | .snippet { 27 | border: 1px solid #ccc; 28 | -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2); 29 | -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2); 30 | box-shadow: 0 0 2px rgba(0,0,0,0.2); 31 | } 32 | 33 | /* error details */ 34 | .error-details { 35 | border-top: 1px solid #FFAAAA; 36 | -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2); 37 | -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2); 38 | box-shadow: 0 0 2px rgba(0,0,0,0.2); 39 | border-bottom: 1px solid #FFAAAA; 40 | -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2); 41 | -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2); 42 | box-shadow: 0 0 2px rgba(0,0,0,0.2); 43 | background-color:#FFF3F3; 44 | line-height: 1.5; 45 | overflow: hidden; 46 | padding: 5px; 47 | padding-left:25px; 48 | } 49 | 50 | .error-details dt { 51 | clear: left; 52 | float: left; 53 | font-weight: bold; 54 | margin-right: 5px; 55 | } 56 | 57 | .error-details dt:after { 58 | content: ":"; 59 | } 60 | 61 | .error-details dd { 62 | display: block; 63 | } 64 | 65 | /* stack trace */ 66 | .stack { 67 | padding: 5px; 68 | overflow: auto; 69 | height: 150px; 70 | } 71 | 72 | /* code snippet */ 73 | .snippet { 74 | background-color: #fff; 75 | font-family: monospace; 76 | } 77 | 78 | .snippet .line { 79 | display: block; 80 | } 81 | 82 | .snippet .lineNumber { 83 | background-color: #ddd; 84 | color: #999; 85 | display: inline-block; 86 | margin-right: 5px; 87 | padding: 0 3px; 88 | text-align: right; 89 | width: 3em; 90 | } 91 | 92 | .snippet .error { 93 | background-color: #fff3f3; 94 | font-weight: bold; 95 | } 96 | 97 | .snippet .error .lineNumber { 98 | background-color: #faa; 99 | color: #333; 100 | font-weight: bold; 101 | } 102 | 103 | .snippet .line:first-child .lineNumber { 104 | padding-top: 5px; 105 | } 106 | 107 | .snippet .line:last-child .lineNumber { 108 | padding-bottom: 5px; 109 | } -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | /* FONT STACK */ 2 | body, 3 | input, select, textarea { 4 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 5 | } 6 | 7 | h1, h2, h3, h4, h5, h6 { 8 | line-height: 1.1; 9 | } 10 | 11 | /* BASE LAYOUT */ 12 | 13 | html { 14 | background-color: #ddd; 15 | background-image: -moz-linear-gradient(center top, #aaa, #ddd); 16 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #aaa), color-stop(1, #ddd)); 17 | background-image: linear-gradient(top, #aaa, #ddd); 18 | filter: progid:DXImageTransform.Microsoft.gradient(startColorStr = '#aaaaaa', EndColorStr = '#dddddd'); 19 | background-repeat: no-repeat; 20 | height: 100%; 21 | /* change the box model to exclude the padding from the calculation of 100% height (IE8+) */ 22 | -webkit-box-sizing: border-box; 23 | -moz-box-sizing: border-box; 24 | box-sizing: border-box; 25 | } 26 | 27 | html.no-cssgradients { 28 | background-color: #aaa; 29 | } 30 | 31 | .ie6 html { 32 | height: 100%; 33 | } 34 | 35 | html * { 36 | margin: 0; 37 | } 38 | 39 | body { 40 | background: #ffffff; 41 | color: #333333; 42 | margin: 0 auto; 43 | max-width: 960px; 44 | overflow-x: hidden; /* prevents box-shadow causing a horizontal scrollbar in firefox when viewport < 960px wide */ 45 | -moz-box-shadow: 0 0 0.3em #255b17; 46 | -webkit-box-shadow: 0 0 0.3em #255b17; 47 | box-shadow: 0 0 0.3em #255b17; 48 | } 49 | 50 | #grailsLogo { 51 | background-color: #abbf78; 52 | } 53 | 54 | /* replace with .no-boxshadow body if you have modernizr available */ 55 | .ie6 body, 56 | .ie7 body, 57 | .ie8 body { 58 | border-color: #255b17; 59 | border-style: solid; 60 | border-width: 0 1px; 61 | } 62 | 63 | .ie6 body { 64 | height: 100%; 65 | } 66 | 67 | a:link, a:visited, a:hover { 68 | color: #48802c; 69 | } 70 | 71 | a:hover, a:active { 72 | outline: none; /* prevents outline in webkit on active links but retains it for tab focus */ 73 | } 74 | 75 | h1 { 76 | color: #48802c; 77 | font-weight: normal; 78 | font-size: 1.25em; 79 | margin: 0.8em 0 0.3em 0; 80 | } 81 | 82 | ul { 83 | padding: 0; 84 | } 85 | 86 | img { 87 | border: 0; 88 | } 89 | 90 | /* GENERAL */ 91 | 92 | #grailsLogo a { 93 | display: inline-block; 94 | margin: 1em; 95 | } 96 | 97 | .content { 98 | } 99 | 100 | .content h1 { 101 | border-bottom: 1px solid #CCCCCC; 102 | margin: 0.8em 1em 0.3em; 103 | padding: 0 0.25em; 104 | } 105 | 106 | .scaffold-list h1 { 107 | border: none; 108 | } 109 | 110 | .footer { 111 | background: #abbf78; 112 | color: #000; 113 | clear: both; 114 | font-size: 0.8em; 115 | margin-top: 1.5em; 116 | padding: 1em; 117 | min-height: 1em; 118 | } 119 | 120 | .footer a { 121 | color: #255b17; 122 | } 123 | 124 | .spinner { 125 | background: url(../images/spinner.gif) 50% 50% no-repeat transparent; 126 | height: 16px; 127 | width: 16px; 128 | padding: 0.5em; 129 | position: absolute; 130 | right: 0; 131 | top: 0; 132 | text-indent: -9999px; 133 | } 134 | 135 | /* NAVIGATION MENU */ 136 | 137 | .nav { 138 | background-color: #efefef; 139 | padding: 0.5em 0.75em; 140 | -moz-box-shadow: 0 0 3px 1px #aaaaaa; 141 | -webkit-box-shadow: 0 0 3px 1px #aaaaaa; 142 | box-shadow: 0 0 3px 1px #aaaaaa; 143 | zoom: 1; 144 | } 145 | 146 | .nav ul { 147 | overflow: hidden; 148 | padding-left: 0; 149 | zoom: 1; 150 | } 151 | 152 | .nav li { 153 | display: block; 154 | float: left; 155 | list-style-type: none; 156 | margin-right: 0.5em; 157 | padding: 0; 158 | } 159 | 160 | .nav a { 161 | color: #666666; 162 | display: block; 163 | padding: 0.25em 0.7em; 164 | text-decoration: none; 165 | -moz-border-radius: 0.3em; 166 | -webkit-border-radius: 0.3em; 167 | border-radius: 0.3em; 168 | } 169 | 170 | .nav a:active, .nav a:visited { 171 | color: #666666; 172 | } 173 | 174 | .nav a:focus, .nav a:hover { 175 | background-color: #999999; 176 | color: #ffffff; 177 | outline: none; 178 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8); 179 | } 180 | 181 | .no-borderradius .nav a:focus, .no-borderradius .nav a:hover { 182 | background-color: transparent; 183 | color: #444444; 184 | text-decoration: underline; 185 | } 186 | 187 | .nav a.home, .nav a.list, .nav a.create { 188 | background-position: 0.7em center; 189 | background-repeat: no-repeat; 190 | text-indent: 25px; 191 | } 192 | 193 | .nav a.home { 194 | background-image: url(../images/skin/house.png); 195 | } 196 | 197 | .nav a.list { 198 | background-image: url(../images/skin/database_table.png); 199 | } 200 | 201 | .nav a.create { 202 | background-image: url(../images/skin/database_add.png); 203 | } 204 | 205 | /* CREATE/EDIT FORMS AND SHOW PAGES */ 206 | 207 | fieldset, 208 | .property-list { 209 | margin: 0.6em 1.25em 0 1.25em; 210 | padding: 0.3em 1.8em 1.25em; 211 | position: relative; 212 | zoom: 1; 213 | border: none; 214 | } 215 | 216 | .property-list .fieldcontain { 217 | list-style: none; 218 | overflow: hidden; 219 | zoom: 1; 220 | } 221 | 222 | .fieldcontain { 223 | margin-top: 1em; 224 | } 225 | 226 | .fieldcontain label, 227 | .fieldcontain .property-label { 228 | color: #666666; 229 | text-align: right; 230 | width: 25%; 231 | } 232 | 233 | .fieldcontain .property-label { 234 | float: left; 235 | } 236 | 237 | .fieldcontain .property-value { 238 | display: block; 239 | margin-left: 27%; 240 | } 241 | 242 | label { 243 | cursor: pointer; 244 | display: inline-block; 245 | margin: 0 0.25em 0 0; 246 | } 247 | 248 | input, select, textarea { 249 | background-color: #fcfcfc; 250 | border: 1px solid #cccccc; 251 | font-size: 1em; 252 | padding: 0.2em 0.4em; 253 | } 254 | 255 | select { 256 | padding: 0.2em 0.2em 0.2em 0; 257 | } 258 | 259 | select[multiple] { 260 | vertical-align: top; 261 | } 262 | 263 | textarea { 264 | width: 250px; 265 | height: 150px; 266 | overflow: auto; /* IE always renders vertical scrollbar without this */ 267 | vertical-align: top; 268 | } 269 | 270 | input[type=checkbox], input[type=radio] { 271 | background-color: transparent; 272 | border: 0; 273 | padding: 0; 274 | } 275 | 276 | input:focus, select:focus, textarea:focus { 277 | background-color: #ffffff; 278 | border: 1px solid #eeeeee; 279 | outline: 0; 280 | -moz-box-shadow: 0 0 0.5em #ffffff; 281 | -webkit-box-shadow: 0 0 0.5em #ffffff; 282 | box-shadow: 0 0 0.5em #ffffff; 283 | } 284 | 285 | .required-indicator { 286 | color: #48802C; 287 | display: inline-block; 288 | font-weight: bold; 289 | margin-left: 0.3em; 290 | position: relative; 291 | top: 0.1em; 292 | } 293 | 294 | ul.one-to-many { 295 | display: inline-block; 296 | list-style-position: inside; 297 | vertical-align: top; 298 | } 299 | 300 | .ie6 ul.one-to-many, .ie7 ul.one-to-many { 301 | display: inline; 302 | zoom: 1; 303 | } 304 | 305 | ul.one-to-many li.add { 306 | list-style-type: none; 307 | } 308 | 309 | /* EMBEDDED PROPERTIES */ 310 | 311 | fieldset.embedded { 312 | background-color: transparent; 313 | border: 1px solid #CCCCCC; 314 | margin-left: 0; 315 | margin-right: 0; 316 | padding-left: 0; 317 | padding-right: 0; 318 | -moz-box-shadow: none; 319 | -webkit-box-shadow: none; 320 | box-shadow: none; 321 | } 322 | 323 | fieldset.embedded legend { 324 | margin: 0 1em; 325 | } 326 | 327 | /* MESSAGES AND ERRORS */ 328 | 329 | .errors, 330 | .message { 331 | font-size: 0.8em; 332 | line-height: 2; 333 | margin: 1em 2em; 334 | padding: 0.25em; 335 | } 336 | 337 | .message { 338 | background: #f3f3ff; 339 | border: 1px solid #b2d1ff; 340 | color: #006dba; 341 | -moz-box-shadow: 0 0 0.25em #b2d1ff; 342 | -webkit-box-shadow: 0 0 0.25em #b2d1ff; 343 | box-shadow: 0 0 0.25em #b2d1ff; 344 | } 345 | 346 | .errors { 347 | background: #fff3f3; 348 | border: 1px solid #ffaaaa; 349 | color: #cc0000; 350 | -moz-box-shadow: 0 0 0.25em #ff8888; 351 | -webkit-box-shadow: 0 0 0.25em #ff8888; 352 | box-shadow: 0 0 0.25em #ff8888; 353 | } 354 | 355 | .errors ul, 356 | .message { 357 | padding: 0; 358 | } 359 | 360 | .errors li { 361 | list-style: none; 362 | background: transparent url(../images/skin/exclamation.png) 0.5em 50% no-repeat; 363 | text-indent: 2.2em; 364 | } 365 | 366 | .message { 367 | background: transparent url(../images/skin/information.png) 0.5em 50% no-repeat; 368 | text-indent: 2.2em; 369 | } 370 | 371 | /* form fields with errors */ 372 | 373 | .error input, .error select, .error textarea { 374 | background: #fff3f3; 375 | border-color: #ffaaaa; 376 | color: #cc0000; 377 | } 378 | 379 | .error input:focus, .error select:focus, .error textarea:focus { 380 | -moz-box-shadow: 0 0 0.5em #ffaaaa; 381 | -webkit-box-shadow: 0 0 0.5em #ffaaaa; 382 | box-shadow: 0 0 0.5em #ffaaaa; 383 | } 384 | 385 | /* same effects for browsers that support HTML5 client-side validation (these have to be specified separately or IE will ignore the entire rule) */ 386 | 387 | input:invalid, select:invalid, textarea:invalid { 388 | background: #fff3f3; 389 | border-color: #ffaaaa; 390 | color: #cc0000; 391 | } 392 | 393 | input:invalid:focus, select:invalid:focus, textarea:invalid:focus { 394 | -moz-box-shadow: 0 0 0.5em #ffaaaa; 395 | -webkit-box-shadow: 0 0 0.5em #ffaaaa; 396 | box-shadow: 0 0 0.5em #ffaaaa; 397 | } 398 | 399 | /* TABLES */ 400 | 401 | table { 402 | border-top: 1px solid #DFDFDF; 403 | border-collapse: collapse; 404 | width: 100%; 405 | margin-bottom: 1em; 406 | } 407 | 408 | tr { 409 | border: 0; 410 | } 411 | 412 | tr>td:first-child, tr>th:first-child { 413 | padding-left: 1.25em; 414 | } 415 | 416 | tr>td:last-child, tr>th:last-child { 417 | padding-right: 1.25em; 418 | } 419 | 420 | td, th { 421 | line-height: 1.5em; 422 | padding: 0.5em 0.6em; 423 | text-align: left; 424 | vertical-align: top; 425 | } 426 | 427 | th { 428 | background-color: #efefef; 429 | background-image: -moz-linear-gradient(top, #ffffff, #eaeaea); 430 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #ffffff), color-stop(1, #eaeaea)); 431 | filter: progid:DXImageTransform.Microsoft.gradient(startColorStr = '#ffffff', EndColorStr = '#eaeaea'); 432 | -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#ffffff', EndColorStr='#eaeaea')"; 433 | color: #666666; 434 | font-weight: bold; 435 | line-height: 1.7em; 436 | padding: 0.2em 0.6em; 437 | } 438 | 439 | thead th { 440 | white-space: nowrap; 441 | } 442 | 443 | th a { 444 | display: block; 445 | text-decoration: none; 446 | } 447 | 448 | th a:link, th a:visited { 449 | color: #666666; 450 | } 451 | 452 | th a:hover, th a:focus { 453 | color: #333333; 454 | } 455 | 456 | th.sortable a { 457 | background-position: right; 458 | background-repeat: no-repeat; 459 | padding-right: 1.1em; 460 | } 461 | 462 | th.asc a { 463 | background-image: url(../images/skin/sorted_asc.gif); 464 | } 465 | 466 | th.desc a { 467 | background-image: url(../images/skin/sorted_desc.gif); 468 | } 469 | 470 | .odd { 471 | background: #f7f7f7; 472 | } 473 | 474 | .even { 475 | background: #ffffff; 476 | } 477 | 478 | th:hover, tr:hover { 479 | background: #E1F2B6; 480 | } 481 | 482 | /* PAGINATION */ 483 | 484 | .pagination { 485 | border-top: 0; 486 | margin: 0; 487 | padding: 0.3em 0.2em; 488 | text-align: center; 489 | -moz-box-shadow: 0 0 3px 1px #AAAAAA; 490 | -webkit-box-shadow: 0 0 3px 1px #AAAAAA; 491 | box-shadow: 0 0 3px 1px #AAAAAA; 492 | background-color: #EFEFEF; 493 | } 494 | 495 | .pagination a, 496 | .pagination .currentStep { 497 | color: #666666; 498 | display: inline-block; 499 | margin: 0 0.1em; 500 | padding: 0.25em 0.7em; 501 | text-decoration: none; 502 | -moz-border-radius: 0.3em; 503 | -webkit-border-radius: 0.3em; 504 | border-radius: 0.3em; 505 | } 506 | 507 | .pagination a:hover, .pagination a:focus, 508 | .pagination .currentStep { 509 | background-color: #999999; 510 | color: #ffffff; 511 | outline: none; 512 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8); 513 | } 514 | 515 | .no-borderradius .pagination a:hover, .no-borderradius .pagination a:focus, 516 | .no-borderradius .pagination .currentStep { 517 | background-color: transparent; 518 | color: #444444; 519 | text-decoration: underline; 520 | } 521 | 522 | /* ACTION BUTTONS */ 523 | 524 | .buttons { 525 | background-color: #efefef; 526 | overflow: hidden; 527 | padding: 0.3em; 528 | -moz-box-shadow: 0 0 3px 1px #aaaaaa; 529 | -webkit-box-shadow: 0 0 3px 1px #aaaaaa; 530 | box-shadow: 0 0 3px 1px #aaaaaa; 531 | margin: 0.1em 0 0 0; 532 | border: none; 533 | } 534 | 535 | .buttons input, 536 | .buttons a { 537 | background-color: transparent; 538 | border: 0; 539 | color: #666666; 540 | cursor: pointer; 541 | display: inline-block; 542 | margin: 0 0.25em 0; 543 | overflow: visible; 544 | padding: 0.25em 0.7em; 545 | text-decoration: none; 546 | 547 | -moz-border-radius: 0.3em; 548 | -webkit-border-radius: 0.3em; 549 | border-radius: 0.3em; 550 | } 551 | 552 | .buttons input:hover, .buttons input:focus, 553 | .buttons a:hover, .buttons a:focus { 554 | background-color: #999999; 555 | color: #ffffff; 556 | outline: none; 557 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8); 558 | -moz-box-shadow: none; 559 | -webkit-box-shadow: none; 560 | box-shadow: none; 561 | } 562 | 563 | .no-borderradius .buttons input:hover, .no-borderradius .buttons input:focus, 564 | .no-borderradius .buttons a:hover, .no-borderradius .buttons a:focus { 565 | background-color: transparent; 566 | color: #444444; 567 | text-decoration: underline; 568 | } 569 | 570 | .buttons .delete, .buttons .edit, .buttons .save { 571 | background-position: 0.7em center; 572 | background-repeat: no-repeat; 573 | text-indent: 25px; 574 | } 575 | 576 | .ie6 .buttons input.delete, .ie6 .buttons input.edit, .ie6 .buttons input.save, 577 | .ie7 .buttons input.delete, .ie7 .buttons input.edit, .ie7 .buttons input.save { 578 | padding-left: 36px; 579 | } 580 | 581 | .buttons .delete { 582 | background-image: url(../images/skin/database_delete.png); 583 | } 584 | 585 | .buttons .edit { 586 | background-image: url(../images/skin/database_edit.png); 587 | } 588 | 589 | .buttons .save { 590 | background-image: url(../images/skin/database_save.png); 591 | } 592 | 593 | a.skip { 594 | position: absolute; 595 | left: -9999px; 596 | } 597 | -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/assets/stylesheets/mobile.css: -------------------------------------------------------------------------------- 1 | /* Styles for mobile devices */ 2 | 3 | @media screen and (max-width: 480px) { 4 | .nav { 5 | padding: 0.5em; 6 | } 7 | 8 | .nav li { 9 | margin: 0 0.5em 0 0; 10 | padding: 0.25em; 11 | } 12 | 13 | /* Hide individual steps in pagination, just have next & previous */ 14 | .pagination .step, .pagination .currentStep { 15 | display: none; 16 | } 17 | 18 | .pagination .prevLink { 19 | float: left; 20 | } 21 | 22 | .pagination .nextLink { 23 | float: right; 24 | } 25 | 26 | /* pagination needs to wrap around floated buttons */ 27 | .pagination { 28 | overflow: hidden; 29 | } 30 | 31 | /* slightly smaller margin around content body */ 32 | fieldset, 33 | .property-list { 34 | padding: 0.3em 1em 1em; 35 | } 36 | 37 | input, textarea { 38 | width: 100%; 39 | -moz-box-sizing: border-box; 40 | -webkit-box-sizing: border-box; 41 | -ms-box-sizing: border-box; 42 | box-sizing: border-box; 43 | } 44 | 45 | select, input[type=checkbox], input[type=radio], input[type=submit], input[type=button], input[type=reset] { 46 | width: auto; 47 | } 48 | 49 | /* hide all but the first column of list tables */ 50 | .scaffold-list td:not(:first-child), 51 | .scaffold-list th:not(:first-child) { 52 | display: none; 53 | } 54 | 55 | .scaffold-list thead th { 56 | text-align: center; 57 | } 58 | 59 | /* stack form elements */ 60 | .fieldcontain { 61 | margin-top: 0.6em; 62 | } 63 | 64 | .fieldcontain label, 65 | .fieldcontain .property-label, 66 | .fieldcontain .property-value { 67 | display: block; 68 | float: none; 69 | margin: 0 0 0.25em 0; 70 | text-align: left; 71 | width: auto; 72 | } 73 | 74 | .errors ul, 75 | .message p { 76 | margin: 0.5em; 77 | } 78 | 79 | .error ul { 80 | margin-left: 0; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/conf/application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | grails: 3 | profile: web 4 | codegen: 5 | defaultPackage: com.example 6 | info: 7 | app: 8 | name: '@info.app.name@' 9 | version: '@info.app.version@' 10 | grailsVersion: '@info.app.grailsVersion@' 11 | spring: 12 | groovy: 13 | template: 14 | check-template-location: false 15 | 16 | --- 17 | grails: 18 | redis: 19 | port: '${REDIS_PORT:6379}' 20 | host: "${REDIS_HOST:localhost}" 21 | connections: 22 | one: 23 | port: 6380 24 | host: "localhost" 25 | two: 26 | port: 6381 27 | host: "localhost" 28 | 29 | --- 30 | grails: 31 | mime: 32 | disable: 33 | accept: 34 | header: 35 | userAgents: 36 | - Gecko 37 | - WebKit 38 | - Presto 39 | - Trident 40 | types: 41 | all: '*/*' 42 | atom: application/atom+xml 43 | css: text/css 44 | csv: text/csv 45 | form: application/x-www-form-urlencoded 46 | html: 47 | - text/html 48 | - application/xhtml+xml 49 | js: text/javascript 50 | json: 51 | - application/json 52 | - text/json 53 | multipartForm: multipart/form-data 54 | rss: application/rss+xml 55 | text: text/plain 56 | hal: 57 | - application/hal+json 58 | - application/hal+xml 59 | xml: 60 | - text/xml 61 | - application/xml 62 | urlmapping: 63 | cache: 64 | maxsize: 1000 65 | controllers: 66 | defaultScope: singleton 67 | converters: 68 | encoding: UTF-8 69 | hibernate: 70 | cache: 71 | queries: false 72 | views: 73 | default: 74 | codec: html 75 | gsp: 76 | encoding: UTF-8 77 | htmlcodec: xml 78 | codecs: 79 | expression: html 80 | scriptlets: html 81 | taglib: none 82 | staticparts: none 83 | 84 | --- 85 | dataSource: 86 | pooled: true 87 | jmxExport: true 88 | driverClassName: org.h2.Driver 89 | username: sa 90 | password: '' 91 | dialect: org.hibernate.dialect.H2Dialect 92 | 93 | environments: 94 | development: 95 | dataSource: 96 | dbCreate: create-drop 97 | url: jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE 98 | test: 99 | dataSource: 100 | dbCreate: update 101 | url: jdbc:h2:mem:testDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE 102 | production: 103 | dataSource: 104 | dbCreate: update 105 | url: jdbc:h2:prodDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE 106 | -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/conf/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UTF-8 6 | %level %logger - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/conf/spring/resources.groovy: -------------------------------------------------------------------------------- 1 | // Place your Spring DSL code here 2 | beans = { 3 | } 4 | -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/controllers/com/example/IndexController.groovy: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | class IndexController { 4 | 5 | BookCreateService bookCreateService 6 | 7 | def index() { 8 | render view: "/index", model: [book:bookCreateService.createOrGetBook()] 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/controllers/com/example/UrlMappings.groovy: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | class UrlMappings { 4 | 5 | static mappings = { 6 | "/$controller/$action?/$id?(.$format)?"{ 7 | constraints { 8 | // apply constraints here 9 | } 10 | } 11 | 12 | "/"(controller: "index", action: "index") 13 | "500"(view:'/error') 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/domain/com/example/Book.groovy: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import grails.plugins.redis.RedisService 4 | import groovy.transform.ToString 5 | import java.time.LocalDate 6 | 7 | @ToString(includes = "id,createDate") 8 | class Book { 9 | 10 | RedisService redisService 11 | 12 | String title = '' 13 | LocalDate createDate = LocalDate.now() 14 | static transients = ['redisService'] 15 | 16 | static mapping = { 17 | autowire true 18 | } 19 | 20 | //todo: FIX THESE ASAP! 21 | // @Memoize(key = '#{title}') 22 | def getMemoizedTitle(LocalDate date) { 23 | redisService?.memoize(title) { 24 | println 'cache miss' 25 | "$title $date" 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/init/com/example/Application.groovy: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import grails.boot.GrailsApp 4 | import grails.boot.config.GrailsAutoConfiguration 5 | import groovy.transform.CompileStatic 6 | 7 | @CompileStatic 8 | class Application extends GrailsAutoConfiguration { 9 | static void main(String[] args) { 10 | GrailsApp.run(Application) 11 | } 12 | } -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/init/com/example/BootStrap.groovy: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | class BootStrap { 4 | 5 | def init = { servletContext -> 6 | } 7 | def destroy = { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/services/com/example/BookCreateService.groovy: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import grails.gorm.transactions.Transactional 4 | 5 | @Transactional 6 | class BookCreateService { 7 | Book createOrGetBook() { 8 | Book b = Book.findOrCreateByTitle('some title') 9 | b.save(flush:true) 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/services/com/example/BookService.groovy: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import grails.plugins.redis.* 4 | import java.time.LocalDate 5 | 6 | class BookService { 7 | 8 | RedisService redisService 9 | 10 | @MemoizeScore(key = '#{map.key}', member = 'foo') 11 | def getAnnotatedScore(Map map) { 12 | println 'cache miss getAnnotatedScore' 13 | return map.foo 14 | } 15 | 16 | @MemoizeList(key = '#{list[0]}') 17 | def getAnnotatedList(List list) { 18 | println 'cache miss getAnnotatedList' 19 | return list 20 | } 21 | 22 | @MemoizeHash(key = '#{map.foo}') 23 | def getAnnotatedHash(Map map) { 24 | println 'cache miss getAnnotatedHash' 25 | return map 26 | } 27 | 28 | @MemoizeHashField(key = '#{map.foo}', member = 'foo') 29 | def getAnnotatedHashField(Map map) { 30 | println 'cache miss getAnnotatedHashField' 31 | return map.foo 32 | } 33 | 34 | 35 | @MemoizeDomainObject(key = '#{title}', clazz = Book.class) 36 | def createDomainObject(String title, LocalDate date) { 37 | println 'cache miss createDomainObject' 38 | def book = new Book(title: title, createDate: date).save(flush: true) 39 | book 40 | } 41 | 42 | 43 | @MemoizeDomainList(key = 'getDomainListWithKeyClass:#{title}', clazz = Book.class) 44 | def getDomainListWithKeyClass(String title, Date date) { 45 | redisService.domainListWithKeyClassKey = "$title $date" 46 | println 'cache miss getDomainListWithKeyClass' 47 | Book.executeQuery("from Book b where b.title = :title", [title: title]) 48 | } 49 | 50 | @Memoize({ '#{text}' }) 51 | def getAnnotatedTextUsingClosure(String text, Date date) { 52 | println 'cache miss getAnnotatedTextUsingClosure' 53 | return "$text $date" 54 | } 55 | 56 | @Memoize(key = '#{text}') 57 | def getAnnotatedTextUsingKey(String text, Date date) { 58 | println 'cache miss getAnnotatedTextUsingKey' 59 | return "$text $date" 60 | } 61 | 62 | //expire this extremely fast to test that it works 63 | @Memoize(key = '#{text}', expire = '1') 64 | def getAnnotatedTextUsingKeyAndExpire(String text, Date date) { 65 | println 'cache miss getAnnotatedTextUsingKeyAndExpire' 66 | return "$text $date" 67 | } 68 | 69 | @Memoize(key = '#{book.title}:#{book.id}') 70 | def getAnnotatedBook(Book book) { 71 | println 'cache miss getAnnotatedBook' 72 | return book.toString() 73 | } 74 | 75 | def getMemoizedTextDate(String text, Date date) { 76 | return redisService.memoize(text) { 77 | println 'cache miss getMemoizedTextDate' 78 | return "$text $date" 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/views/error.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <g:if env="development">Grails Runtime Exception</g:if><g:else>Error</g:else> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/views/index.gsp: -------------------------------------------------------------------------------- 1 | <%@ page import="com.example.Book" %> 2 | 3 | 4 | 5 | 6 | Welcome to Grails 7 | 8 | 9 | ${book.toString()} 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/redis-demo/grails-app/views/layouts/main.gsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <g:layoutTitle default="Grails"/> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/redis-demo/src/test/groovy/com/example/HoldersIntegrationSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import grails.testing.mixin.integration.Integration 4 | import grails.util.Holders 5 | import spock.lang.Specification 6 | 7 | 8 | @Integration 9 | class HoldersIntegrationSpec extends Specification { 10 | 11 | def "ensure holders wires up getGrailsApplication getMainContext in integration tests"(){ 12 | when: 13 | def grailsApplicationMainContext = Holders?.getGrailsApplication()?.getMainContext() 14 | 15 | then: 16 | grailsApplicationMainContext?.getBean('redisService') 17 | } 18 | 19 | def "ensure holders wires up getGrailsApplication getParentContext in integration tests"(){ 20 | when: 21 | def context = Holders?.getGrailsApplication()?.getParentContext() 22 | 23 | then: 24 | context?.getBean('redisService') 25 | } 26 | 27 | def "ensure holders wires up findApplicationContext in integration tests"(){ 28 | when: 29 | def context = Holders?.findApplicationContext() 30 | 31 | then: 32 | context?.getBean('redisService') 33 | } 34 | } -------------------------------------------------------------------------------- /examples/redis-demo/src/test/groovy/com/example/MemoizeAnnotationSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import grails.plugins.redis.RedisService 4 | import grails.testing.mixin.integration.Integration 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import spock.lang.Specification 7 | 8 | @Integration 9 | class MemoizeAnnotationSpec extends Specification { 10 | 11 | @Autowired RedisService redisService 12 | 13 | void setup() { 14 | redisService.flushDB() 15 | } 16 | 17 | void testMemoizeAnnotationExpire() { 18 | given: 19 | // set up test class 20 | def testClass = new GroovyClassLoader().parseClass(''' 21 | import grails.plugins.redis.* 22 | 23 | class TestClass{ 24 | RedisService redisService 25 | 26 | def key 27 | def expire 28 | 29 | @Memoize(key="#{key}", expire="#{expire}") 30 | def testAnnotatedMethod(){ 31 | return "testValue" 32 | } 33 | } 34 | ''') 35 | String testKey = "key123" 36 | String testExpire = "1000" 37 | 38 | when: 39 | def testInstance = testClass.getDeclaredConstructor().newInstance() 40 | 41 | // inject redis service 42 | testInstance.redisService = redisService 43 | testInstance.key = testKey 44 | testInstance.expire = testExpire 45 | 46 | then: 47 | redisService.get("$testKey") == null 48 | 49 | when: 50 | def output = testInstance.testAnnotatedMethod() 51 | 52 | then: 53 | output == 'testValue' 54 | redisService.get("$testKey") == 'testValue' 55 | 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /examples/redis-demo/src/test/groovy/com/example/MemoizeObjectAnnotationSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.google.gson.GsonBuilder 4 | import grails.plugins.redis.MemoizeObject 5 | import grails.plugins.redis.RedisService 6 | import grails.testing.mixin.integration.Integration 7 | import org.springframework.beans.factory.annotation.Autowired 8 | import spock.lang.Specification 9 | 10 | @Integration 11 | class MemoizeObjectAnnotationSpec extends Specification { 12 | 13 | @Autowired RedisService redisService 14 | GsonBuilder gsonBuilder = new GsonBuilder() 15 | 16 | public void setup() { 17 | redisService.flushDB() 18 | } 19 | 20 | void testMemoizeAnnotationExpire() { 21 | given: 22 | // set up test class 23 | def testClass = new GroovyClassLoader().parseClass(''' 24 | import grails.plugins.redis.* 25 | 26 | class TestClass { 27 | def redisService 28 | def gsonBuilder 29 | def key 30 | def expire 31 | 32 | @MemoizeObject(key="#{key}", expire="#{expire}", clazz=Book.class) 33 | def testAnnotatedMethod(String bookTitle, String bookAuthor, Map chapterMap){ 34 | Book book = new Book(author:bookAuthor, title:"Book of $bookTitle") 35 | List chapters = [] 36 | chapterMap.each { chapterTitle, chapterContent -> 37 | chapters << new Chapter(title:chapterTitle, content:chapterContent, length:chapterContent.size()) 38 | } 39 | book.chapters = chapters 40 | return book 41 | } 42 | 43 | private class Book { 44 | String author 45 | String title 46 | List chapters 47 | } 48 | 49 | private class Chapter { 50 | String title 51 | String content 52 | Integer length 53 | } 54 | 55 | } 56 | ''') 57 | String testKey = "key123" 58 | String testExpire = "1000" 59 | 60 | def testInstance = testClass.newInstance() 61 | 62 | // inject redis service 63 | testInstance.redisService = redisService 64 | // inject gsonBuilder service 65 | testInstance.gsonBuilder = gsonBuilder 66 | testInstance.key = testKey 67 | testInstance.expire = testExpire 68 | 69 | when: "create instance of testClass" 70 | def testResult = testInstance.testAnnotatedMethod('Groovy', 'Author', 71 | ['Groovy': 'This is the content', 'Testing': 'Testing is important']) 72 | 73 | then: 74 | //verify stored JSON 75 | redisService."$testKey" == '''{"author":"Author","title":"Book of Groovy","chapters":[{"title":"Groovy","content":"This is the content","length":19},{"title":"Testing","content":"Testing is important","length":20}]}''' 76 | 77 | //verify returned Book 78 | testResult.author == "Author" 79 | testResult.title == "Book of Groovy" 80 | testResult.chapters.size() == 2 81 | testResult.chapters[0].title == "Groovy" 82 | testResult.chapters[1].title == "Testing" 83 | } 84 | 85 | void testMemoizeSimpleObject() { 86 | given: 87 | TestSimpleObject testInstance = new TestSimpleObject(redisService: redisService) 88 | testInstance.callCount == 0 89 | 90 | when: 91 | Long testResult = testInstance.testAnnotatedMethod() 92 | 93 | then: 94 | redisService."${TestSimpleObject.key}" == '''10''' 95 | testResult == TestSimpleObject.value 96 | testInstance.callCount == 1 97 | 98 | when: 99 | testResult = testInstance.testAnnotatedMethod() 100 | 101 | then: 102 | redisService."${TestSimpleObject.key}" == '''10''' 103 | testResult == TestSimpleObject.value 104 | testInstance.callCount == 1 105 | } 106 | } 107 | 108 | 109 | class TestSimpleObject { 110 | def redisService 111 | def callCount = 0 112 | public static final key = "TheKey" 113 | public static final Long value = 10 114 | 115 | @MemoizeObject(key = "#{key}", clazz = Long.class) 116 | def testAnnotatedMethod() { 117 | callCount += 1 118 | return new Long(value) 119 | } 120 | } -------------------------------------------------------------------------------- /examples/redis-demo/src/test/groovy/com/example/ProxyAwareSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | trait ProxyAwareSpec { 4 | 5 | 6 | Serializable getEntityId(Object obj) { 7 | Serializable identifier = proxyHandler.unwrapIfProxy(obj)?.ident() 8 | identifier ?: obj?.id 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /examples/redis-demo/src/test/groovy/com/example/RedisIntegrationSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import grails.core.support.proxy.ProxyHandler 4 | import grails.plugins.redis.RedisService 5 | import grails.testing.mixin.integration.Integration 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.test.annotation.Rollback 8 | import spock.lang.Specification 9 | 10 | import static grails.plugins.redis.RedisService.KEY_DOES_NOT_EXIST 11 | import static grails.plugins.redis.RedisService.NO_EXPIRATION_TTL 12 | 13 | @Integration 14 | @Rollback 15 | class RedisIntegrationSpec extends Specification implements ProxyAwareSpec { 16 | 17 | @Autowired RedisService redisService 18 | @Autowired ProxyHandler proxyHandler 19 | 20 | void setup() { 21 | redisService.flushDB() 22 | } 23 | 24 | void testMemoizeDomainList() { 25 | given: 26 | def book1 = createBook('book1') 27 | def book2 = createBook('book2') 28 | def book3 = createBook('book3') 29 | 30 | def calledCount = 0 31 | def cacheMissClosure = { 32 | calledCount += 1 33 | Book.withNewTransaction { 34 | return Book.executeQuery("from Book b where b.title = 'book1' or b.title = 'book3'") 35 | } 36 | } 37 | 38 | when: 39 | def cacheMissList = redisService.memoizeDomainList(Book, "domainkey", cacheMissClosure) 40 | 41 | then: 42 | 1 == calledCount 43 | [book1, book3]*.id== cacheMissList*.id 44 | NO_EXPIRATION_TTL == redisService.ttl("domainkey") 45 | 46 | when: 47 | def cacheHitList = redisService.memoizeDomainList(Book, "domainkey", cacheMissClosure) 48 | 49 | then: 50 | // cache hit, don't call closure again 51 | 1 == calledCount 52 | [book1, book3]*.id == cacheHitList.collect { getEntityId(it) } 53 | cacheMissList*.id == cacheHitList*.id 54 | } 55 | 56 | 57 | public void testMemoizeDomainListWithExpire() { 58 | given: 59 | def book1 = createBook('book1') 60 | KEY_DOES_NOT_EXIST == redisService.ttl("domainkey") 61 | 62 | when: 63 | def result = redisService.memoizeDomainList(Book, "domainkey", 60) { [book1] } 64 | 65 | then: 66 | [book1] == result 67 | NO_EXPIRATION_TTL < redisService.ttl("domainkey") 68 | } 69 | 70 | 71 | public void testMemoizeDomainIdList() { 72 | given: 73 | def book1 = createBook('book1') 74 | def book2 = createBook('book2') 75 | def book3 = createBook('book3') 76 | 77 | def books = [book1, book3] 78 | 79 | def calledCount = 0 80 | def cacheMissClosure = { 81 | calledCount += 1 82 | return books 83 | } 84 | 85 | when: 86 | def cacheMissList = redisService.memoizeDomainIdList(Book, "domainkey", cacheMissClosure) 87 | 88 | then: 89 | 1 == calledCount 90 | [book1.id, book3.id] == cacheMissList 91 | 92 | when: 93 | def cacheHitList = redisService.memoizeDomainIdList(Book, "domainkey", cacheMissClosure) 94 | 95 | then: 96 | // cache hit, don't call closure again 97 | 1 == calledCount 98 | [book1.id, book3.id] == cacheHitList 99 | cacheMissList == cacheHitList 100 | } 101 | 102 | 103 | public void testMemoizeDomainObject() { 104 | given: 105 | Book book1 = createBook('book1') 106 | 107 | def calledCount = 0 108 | def cacheMissClosure = { 109 | calledCount += 1 110 | Book.withNewTransaction { 111 | return Book.get(book1.id) 112 | } 113 | } 114 | 115 | when: 116 | def cacheMissBook = redisService.memoizeDomainObject(Book, "domainkey", cacheMissClosure) 117 | 118 | then: 119 | 1 == calledCount 120 | book1.id == cacheMissBook.id 121 | 122 | when: 123 | def cacheHitBook = redisService.memoizeDomainObject(Book, "domainkey", cacheMissClosure) 124 | 125 | then: 126 | // cache hit, don't call closure again 127 | 1 == calledCount 128 | book1.id == cacheHitBook.id 129 | cacheHitBook.id == cacheMissBook.id 130 | } 131 | 132 | private static Book createBook(String title) { 133 | Book.withNewTransaction { 134 | return new Book(title: title).save(flush: true) 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /examples/redis-demo/src/test/groovy/com/example/RedisMemoizeDomainSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import grails.gorm.transactions.Rollback 4 | import grails.plugins.redis.RedisService 5 | import grails.testing.mixin.integration.Integration 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import spock.lang.Specification 8 | import java.time.LocalDate 9 | 10 | @Integration 11 | @Rollback 12 | class RedisMemoizeDomainSpec extends Specification { 13 | 14 | @Autowired RedisService redisService 15 | 16 | def setup() { 17 | redisService.flushDB() 18 | } 19 | 20 | def "get AST transformed domain object method"() { 21 | given: 22 | def title = 'all the things' 23 | LocalDate date1 = LocalDate.now() 24 | LocalDate date2 = date1.plusDays(1) 25 | Book book = new Book(title: title).save(flush: true) 26 | 27 | when: 28 | def string1 = book.getMemoizedTitle(date1) 29 | 30 | then: 31 | string1 == "$title $date1" 32 | 33 | when: 34 | def string2 = book.getMemoizedTitle(date2) 35 | 36 | then: 37 | string2 != "$title $date2" 38 | string2 == "$title $date1" 39 | } 40 | } -------------------------------------------------------------------------------- /examples/redis-demo/src/test/groovy/com/example/RedisMemoizeServiceSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import grails.gorm.transactions.Rollback 4 | import grails.plugins.redis.RedisService 5 | import grails.testing.mixin.integration.Integration 6 | import groovy.time.TimeCategory 7 | import org.springframework.beans.factory.annotation.Autowired 8 | import spock.lang.Specification 9 | import java.time.LocalDate 10 | 11 | @Integration 12 | @Rollback 13 | class RedisMemoizeServiceSpec extends Specification { 14 | 15 | @Autowired RedisService redisService 16 | @Autowired BookService bookService 17 | 18 | def setup() { 19 | redisService.flushDB() 20 | } 21 | 22 | def "get AST transformed score using map key"() { 23 | given: 24 | Map map = [key: 'key', foo: 100] 25 | 26 | when: 27 | def score = bookService.getAnnotatedScore(map) 28 | 29 | then: 30 | score == 100 31 | 32 | when: 33 | map.foo = 200 34 | def score2 = bookService.getAnnotatedScore(map) 35 | 36 | then: 37 | score2 == 100 38 | } 39 | 40 | def "get AST transformed list using item 0 as key"() { 41 | given: 42 | def list = ['one', 'two', 'three'] 43 | def list2 = ['one', 'three', 'four'] 44 | 45 | when: 46 | def aList = bookService.getAnnotatedList(list) 47 | 48 | then: 49 | redisService.lrange('one', 0, -1) == list 50 | aList == list 51 | aList[0] == 'one' 52 | aList[1] == 'two' 53 | aList[2] == 'three' 54 | 55 | when: 56 | def aList2 = bookService.getAnnotatedList(list2) 57 | 58 | then: 59 | redisService.lrange('one', 0, -1) == list 60 | redisService.lrange('one', 0, -1) != list2 61 | aList2 == list 62 | aList2[0] == 'one' 63 | aList2[1] == 'two' 64 | aList2[2] == 'three' 65 | } 66 | 67 | def "get AST transformed hash using maps foo as key"() { 68 | given: 69 | def map = [foo: 'foo', bar: 'bar'] 70 | def map2 = [foo: 'foo', bar: 'bar2'] 71 | 72 | when: 73 | def hash = bookService.getAnnotatedHash(map) 74 | 75 | then: 76 | redisService.hgetAll('foo') == map 77 | hash == map 78 | hash.foo == 'foo' 79 | hash.bar == 'bar' 80 | 81 | when: 82 | def hash2 = bookService.getAnnotatedHash(map2) 83 | 84 | then: 85 | redisService.hgetAll('foo') == map 86 | redisService.hgetAll('foo') != map2 87 | hash2 == map 88 | hash2.foo == 'foo' 89 | hash2.bar == 'bar' 90 | } 91 | 92 | 93 | def "get AST transformed hash field using maps foo as key"() { 94 | given: 95 | def map = [foo: 'foo', bar: 'bar'] 96 | def map2 = [foo: 'foo', bar: 'bar2'] 97 | 98 | when: 99 | def hash = bookService.getAnnotatedHash(map) 100 | def fieldValue = bookService.getAnnotatedHashField(map) 101 | 102 | then: 103 | redisService.hget('foo', 'foo') == 'foo' 104 | redisService.hget('foo', 'bar') == 'bar' 105 | redisService.hgetAll('foo') == map 106 | fieldValue == 'foo' 107 | hash == map 108 | hash.foo == 'foo' 109 | hash.bar == 'bar' 110 | 111 | when: 112 | def fieldValue2 = bookService.getAnnotatedHashField(map2) 113 | def hash2 = bookService.getAnnotatedHash(map2) 114 | 115 | then: 116 | redisService.hget('foo', 'foo') == 'foo' 117 | redisService.hget('foo','bar') == 'bar' 118 | fieldValue2 == 'foo' 119 | redisService.hgetAll('foo') == map 120 | redisService.hgetAll('foo') != map2 121 | hash2 == map 122 | hash2.foo == 'foo' 123 | hash2.bar == 'bar' 124 | } 125 | 126 | def "get AST transformed domain object using title"() { 127 | given: 128 | String title = 'ted' 129 | LocalDate date = LocalDate.now() 130 | 131 | when: 132 | Book book = bookService.createDomainObject(title, date) 133 | 134 | then: 135 | redisService.ted == book.id.toString() 136 | book.title == title 137 | book.createDate == date 138 | 139 | when: 140 | Book book2 = bookService.createDomainObject(title, (date)) 141 | 142 | then: 143 | book2.title == title 144 | book2.createDate == date 145 | book2.createDate != plusDays(date, 1) 146 | 147 | when: 'change the title and it should get a new book' 148 | Book book3 = bookService.createDomainObject(title + '2', date) 149 | 150 | then: 151 | redisService.ted2 == book3.id.toString() 152 | book3.title == title + '2' 153 | book3.createDate == date 154 | 155 | } 156 | 157 | def "get AST transformed domain list using key and class"() { 158 | given: 159 | def title = 'narwhals' 160 | def books = [] 161 | Date date1 = new Date(), date2 = plusDays(new Date(), 1) 162 | 10.times { 163 | books << new Book(title: title).save(flush: true) 164 | } 165 | 166 | when: 167 | def list1 = bookService.getDomainListWithKeyClass(title, date1) 168 | 169 | then: 170 | redisService.domainListWithKeyClassKey == "$title $date1" 171 | list1.size() == 10 172 | list1.containsAll(books) 173 | 174 | when: 'calling again should not invoke cache miss and key should remain unchanged' 175 | def list2 = bookService.getDomainListWithKeyClass(title, date2) 176 | 177 | then: 178 | redisService.domainListWithKeyClassKey == "$title $date1" 179 | list2.size() == 10 180 | list2.containsAll(books) 181 | } 182 | 183 | def "get AST transformed method using object property key"() { 184 | given: 185 | def title = 'narwhals' 186 | LocalDate date = LocalDate.now() 187 | Book book = new Book(title: title, createDate: date).save(flush: true, failOnError:true) 188 | def bookString1 = book.toString() 189 | 190 | when: 'get the initial value and cache it' 191 | def value1 = bookService.getAnnotatedBook(book) 192 | 193 | then: 194 | value1 == bookString1 195 | 196 | when: 'change some non-key prop on book and get again' 197 | book.createDate = plusDays(book.createDate, 1) 198 | def bookString2 = book.toString() 199 | def value2 = bookService.getAnnotatedBook(book) 200 | 201 | then: 'value should be the same as first call due to overlapping keys' 202 | value2 == bookString1 203 | value2 != bookString2 204 | } 205 | 206 | 207 | def "get AST transformed method using simple string key property and expire"() { 208 | given: 209 | def text = 'hello' 210 | Date date = new Date() 211 | Date date2 = plusDays(date, 1) 212 | 213 | when: 'get the initial value and cache it' 214 | def value1 = bookService.getAnnotatedTextUsingKeyAndExpire(text, date) 215 | Thread.sleep(2000) //give redis sometime to expire 1ms ttl 216 | def value2 = bookService.getAnnotatedTextUsingKeyAndExpire(text, date2) 217 | 218 | then: 'value should be the same as first call not new date' 219 | value1 == "$text $date" 220 | value2 == "$text $date2" 221 | } 222 | 223 | def "get AST transformed method using simple string key property"() { 224 | given: 225 | def text = 'hello' 226 | Date date = new Date() 227 | Date date2 = plusDays(date, 1) 228 | 229 | when: 'get the initial value and cache it' 230 | def value1 = bookService.getAnnotatedTextUsingKey(text, date) 231 | def value2 = bookService.getAnnotatedTextUsingKey(text, date2) 232 | 233 | then: 'value should be the same as first call not new date' 234 | value1 == "$text $date" 235 | value2 == "$text $date" 236 | value2 != "$text $date2" 237 | } 238 | 239 | def "get AST transformed method using simple key closure"() { 240 | given: 241 | def text = 'hello' 242 | Date date = new Date() 243 | Date date2 = plusDays(date, 1) 244 | 245 | when: 'get the value and cache it' 246 | def value1 = bookService.getAnnotatedTextUsingClosure(text, date) 247 | def value2 = bookService.getAnnotatedTextUsingClosure(text, date2) 248 | 249 | then: 'value should be the same as first call not new date' 250 | value1 == "$text $date" 251 | value2 == "$text $date" 252 | value2 != "$text $date2" 253 | } 254 | 255 | def "make sure redis is bahving correctly on non-annotated methods"() { 256 | given: 257 | def text = 'hello' 258 | Date date = new Date() 259 | Date date2 = plusDays(date, 1) 260 | 261 | when: 'get the value and cache it' 262 | def value1 = bookService.getMemoizedTextDate(text, date) 263 | def value2 = bookService.getMemoizedTextDate(text, date2) 264 | 265 | then: 'value should be the same as first call not new date' 266 | value1 == "$text $date" 267 | value2 == value1 268 | value2 == "$text $date" 269 | value2 != "$text $date2" 270 | } 271 | 272 | private LocalDate plusDays(LocalDate date, Integer number) { 273 | date.plusDays(number) 274 | } 275 | 276 | private Date plusDays(Date date, Integer number) { 277 | use(TimeCategory) { 278 | return (date + number.days) 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /examples/redis-demo/src/test/groovy/com/example/RedisMultipleConnectionsConfigSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import grails.plugins.redis.RedisService 4 | import org.springframework.beans.factory.annotation.Autowired 5 | import spock.lang.Specification 6 | import spock.lang.Ignore 7 | import redis.clients.jedis.Jedis 8 | 9 | /** 10 | */ 11 | @Ignore // if not ignored, this spec expects that additional redis instances are running on localhost ports 6380 and 6381 and will fail without them 12 | class RedisMultipleConnectionsConfigSpec extends Specification { 13 | 14 | @Autowired 15 | RedisService redisService 16 | @Autowired 17 | RedisService redisServiceOne 18 | @Autowired 19 | RedisService redisServiceTwo 20 | 21 | def setup() { 22 | redisService.flushDB() 23 | try { 24 | redisService.withConnection('one').flushDB() 25 | } catch(redis.clients.jedis.exceptions.JedisConnectionException e) { 26 | throw new Exception("You need to have redis running on port 6380 for this test exercising multiple redis connections to pass", e) 27 | } 28 | 29 | try { 30 | redisService.withConnection('two').flushDB() 31 | } catch(redis.clients.jedis.exceptions.JedisConnectionException e) { 32 | throw new Exception("You need to have redis running on port 6381 for this test exercising multiple redis connections to pass", e) 33 | } 34 | 35 | redisServiceOne.flushDB() //redundant, but just illustrates another way to do this 36 | redisServiceTwo.flushDB() //redundant, but just illustrates another way to do this 37 | } 38 | 39 | // if this test is failing, make sure you've got redis running on ports 6380 and 6381 40 | def "test multiple redis pools"() { 41 | given: 42 | def key = "key" 43 | def data = "data" 44 | 45 | when: 46 | redisService.withConnection('one').withRedis {Jedis redis -> 47 | redis.set(key, data) 48 | } 49 | 50 | then: 'These will both use the same redis connection' 51 | redisService.withConnection('one').withRedis {Jedis redis -> 52 | redis.get(key) == data 53 | } 54 | redisServiceOne.withRedis {Jedis redis -> 55 | redis.get(key) == data 56 | } 57 | 58 | and: 59 | redisService.withConnection('two').withRedis {Jedis redis -> 60 | !redis.get(key) 61 | } 62 | redisServiceTwo.withRedis {Jedis redis -> 63 | !redis.get(key) 64 | } 65 | redisService.withRedis {Jedis redis -> 66 | !redis.get(key) 67 | } 68 | redisService.withConnection('blahblahblah').withRedis {Jedis redis -> 69 | !redis.get(key) 70 | } 71 | 72 | when: 73 | redisService.withConnection('two').withRedis {Jedis redis -> 74 | redis.set(key, data) 75 | } 76 | 77 | then: 78 | redisService.withConnection('one').withRedis {Jedis redis -> 79 | redis.get(key) == data 80 | } 81 | redisServiceOne.withRedis {Jedis redis -> 82 | redis.get(key) == data 83 | } 84 | redisService.withConnection('two').withRedis {Jedis redis -> 85 | redis.get(key) == data 86 | } 87 | redisServiceTwo.withRedis {Jedis redis -> 88 | redis.get(key) == data 89 | } 90 | 91 | and: 92 | redisService.withRedis {Jedis redis -> 93 | !redis.get(key) 94 | } 95 | redisService.withConnection('blahblahblah').withRedis {Jedis redis -> 96 | !redis.get(key) 97 | } 98 | 99 | 100 | when: 101 | redisService.withRedis {Jedis redis -> 102 | redis.set(key, data) 103 | } 104 | 105 | then: 106 | redisService.withConnection('one').withRedis {Jedis redis -> 107 | redis.get(key) == data 108 | } 109 | redisService.withConnection('two').withRedis {Jedis redis -> 110 | redis.get(key) == data 111 | } 112 | redisService.withRedis {Jedis redis -> 113 | redis.get(key) == data 114 | } 115 | 116 | //will use default connection just lke normal redisService.withRedis since blahblahblah isn't valid 117 | redisService.withConnection('blahblahblah').withRedis {Jedis redis -> 118 | redis.get(key) == data 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | projectVersion=5.0.0-SNAPSHOT 2 | 3 | grailsVersion=7.0.0-SNAPSHOT 4 | javaVersion=17 5 | 6 | # This prevents the Grails Gradle Plugin from unnecessarily excluding slf4j-simple in the generated POMs 7 | # https://github.com/apache/grails-gradle-plugin/issues/222 8 | slf4jPreventExclusion=true 9 | 10 | org.gradle.caching=true 11 | org.gradle.daemon=true 12 | org.gradle.parallel=true 13 | org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024M 14 | -------------------------------------------------------------------------------- /gradle/docs-config.gradle: -------------------------------------------------------------------------------- 1 | configurations.register('documentation') { 2 | extendsFrom(configurations.compileClasspath) 3 | } 4 | 5 | dependencies { 6 | add('documentation', 'com.github.javaparser:javaparser-core') 7 | } 8 | 9 | tasks.named('groovydoc', Groovydoc) { 10 | groovyClasspath += configurations.documentation 11 | } -------------------------------------------------------------------------------- /gradle/examples-config.gradle: -------------------------------------------------------------------------------- 1 | tasks.withType(Groovydoc).configureEach { 2 | enabled = false 3 | } -------------------------------------------------------------------------------- /gradle/java-config.gradle: -------------------------------------------------------------------------------- 1 | compileJava.options.release = javaVersion.toInteger() -------------------------------------------------------------------------------- /gradle/publish-config.gradle: -------------------------------------------------------------------------------- 1 | import org.grails.gradle.plugin.publishing.GrailsPublishExtension 2 | 3 | extensions.configure(GrailsPublishExtension) { 4 | // Explicit `it` is required here 5 | it.githubSlug = 'apache/grails-redis' 6 | it.license.name = 'Apache-2.0' 7 | it.title = 'Grails Redis Plugin' 8 | it.desc = 'This Plugin provides access to Redis and various utilities (service, annotations, etc) for caching.' 9 | it.developers = [ 10 | tednaleid: 'Ted Naleid', 11 | burtbeckwith: 'Burt Beckwith', 12 | christianoestreich: 'Christian Oestreich', 13 | briancoles: 'Brian Coles', 14 | michaelcameron: 'Michael Cameron', 15 | johnengelman: 'John Engelman', 16 | davidseiler: 'David Seiler', 17 | jordonsaardchit: 'Jordon Saardchit', 18 | florianlangenhahn: 'Florian Langenhahn', 19 | germansancho: 'German Sancho', 20 | johnmulhern: 'John Mulhern', 21 | shaunjurgemeyer: 'Shaun Jurgemeyer', 22 | puneetbehl: 'Puneet Behl' 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /gradle/test-config.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | add('testRuntimeOnly', 'org.junit.platform:junit-platform-launcher') // Gradle 9+ requires this 3 | } 4 | 5 | tasks.withType(Test).configureEach { 6 | useJUnitPlatform() 7 | testLogging { 8 | exceptionFormat = 'full' 9 | events 'passed', 'skipped', 'failed'//, 'standardOut', 'standardError' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/grails-redis/b566937d14216241826401a39c08e2b5e53a5f28/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 | -------------------------------------------------------------------------------- /plugin/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'org.apache.grails.gradle.grails-plugin' 4 | id 'org.apache.grails.gradle.grails-gsp' 5 | } 6 | 7 | version = projectVersion 8 | group = 'org.apache.grails' 9 | 10 | dependencies { 11 | 12 | implementation platform("org.apache.grails:grails-bom:$grailsVersion") 13 | 14 | api 'redis.clients:jedis', { 15 | // api: Jedis, JedisConnectionException 16 | // impl: JedisPool, JedisPoolConfig, JedisSentinelPool, Pipeline, Protocol, Transaction 17 | } 18 | api 'com.google.code.gson:gson', { 19 | // api: Gson(transform) 20 | } 21 | api 'org.springframework:spring-beans', { 22 | // api: @Autowired(transform) 23 | } 24 | api 'org.springframework:spring-core', { 25 | // api: Opcodes(transform) 26 | } 27 | 28 | implementation 'org.apache.grails.data:grails-datamapping-core', { 29 | // impl: @Transactional(runtime) 30 | } 31 | implementation 'org.apache.grails.views:grails-web-taglib', { 32 | // Project has a taglib 33 | } 34 | 35 | compileOnly 'org.apache.grails.bootstrap:grails-bootstrap', { 36 | // comp: @PluginSource(source) 37 | } 38 | compileOnly 'org.apache.grails:grails-core', { // Provided as this is a Grails Plugin 39 | // api GrailsApplication, Holders(transform) 40 | // impl: GrailsApp, GrailsAutoConfiguration, Plugin 41 | } 42 | compileOnly 'org.apache.groovy:groovy' // Provided as this is a Grails Plugin 43 | 44 | testImplementation 'org.apache.grails:grails-testing-support-web' 45 | testImplementation 'org.spockframework:spock-core' 46 | } 47 | 48 | apply { 49 | from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle') 50 | from rootProject.layout.projectDirectory.file('gradle/java-config.gradle') 51 | from rootProject.layout.projectDirectory.file('gradle/publish-config.gradle') 52 | from rootProject.layout.projectDirectory.file('gradle/test-config.gradle') 53 | } -------------------------------------------------------------------------------- /plugin/grails-app/conf/application.yml: -------------------------------------------------------------------------------- 1 | grails: 2 | profile: plugin 3 | codegen: 4 | defaultPackage: redis 5 | info: 6 | app: 7 | name: '@info.app.name@' 8 | version: '@info.app.version@' 9 | grailsVersion: '@info.app.grailsVersion@' 10 | spring: 11 | groovy: 12 | template: 13 | check-template-location: false 14 | 15 | --- 16 | grails: 17 | redis: 18 | port: '${REDIS_PORT:6379}' 19 | host: '${REDIS_HOST:localhost}' 20 | poolConfig: 21 | maxIdle: 1 22 | maxTotal: 10 23 | doesnotexist: true -------------------------------------------------------------------------------- /plugin/grails-app/conf/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UTF-8 6 | %level %logger - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /plugin/grails-app/init/redis/Application.groovy: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import grails.boot.GrailsApp 4 | import grails.boot.config.GrailsAutoConfiguration 5 | import grails.plugins.metadata.PluginSource 6 | import groovy.transform.CompileStatic 7 | 8 | @PluginSource 9 | @CompileStatic 10 | class Application extends GrailsAutoConfiguration { 11 | static void main(String[] args) { 12 | GrailsApp.run(Application) 13 | } 14 | } -------------------------------------------------------------------------------- /plugin/grails-app/taglib/grails/plugins/redis/RedisTagLib.groovy: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2011 SpringSource 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package grails.plugins.redis 16 | 17 | import org.springframework.beans.factory.annotation.Autowired 18 | 19 | class RedisTagLib { 20 | 21 | @Autowired 22 | RedisService redisService 23 | 24 | static namespace = 'redis' 25 | 26 | def memoize = { attrs, body -> 27 | String key = attrs.key 28 | if (!key) throw new IllegalArgumentException('[key] attribute must be specified for memoize!') 29 | 30 | Integer expire = attrs.expire?.toInteger() 31 | 32 | out << redisService.memoize(key, expire) { body() ?: '' } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/Memoize.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis 2 | 3 | import java.lang.annotation.ElementType 4 | import java.lang.annotation.Retention 5 | import java.lang.annotation.RetentionPolicy 6 | import java.lang.annotation.Target 7 | import org.codehaus.groovy.transform.GroovyASTTransformationClass 8 | 9 | /** 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target([ElementType.METHOD]) 13 | @GroovyASTTransformationClass(['grails.plugins.redis.ast.MemoizeASTTransformation']) 14 | @interface Memoize { 15 | Class value() default {true}; 16 | String key() default ''; 17 | String expire() default ''; 18 | } 19 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/MemoizeDomainList.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis 2 | 3 | import java.lang.annotation.ElementType 4 | import java.lang.annotation.Retention 5 | import java.lang.annotation.RetentionPolicy 6 | import java.lang.annotation.Target 7 | import org.codehaus.groovy.transform.GroovyASTTransformationClass 8 | 9 | /** 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target([ElementType.METHOD]) 13 | @GroovyASTTransformationClass(['grails.plugins.redis.ast.MemoizeDomainListASTTransformation']) 14 | @interface MemoizeDomainList { 15 | Class clazz() default {}; 16 | String key() default ''; 17 | String expire() default ''; 18 | } 19 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/MemoizeDomainObject.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis 2 | 3 | import java.lang.annotation.ElementType 4 | import java.lang.annotation.Retention 5 | import java.lang.annotation.RetentionPolicy 6 | import java.lang.annotation.Target 7 | import org.codehaus.groovy.transform.GroovyASTTransformationClass 8 | 9 | /** 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target([ElementType.METHOD]) 13 | @GroovyASTTransformationClass(['grails.plugins.redis.ast.MemoizeDomainObjectASTTransformation']) 14 | @interface MemoizeDomainObject { 15 | Class clazz() default {}; 16 | String key() default ''; 17 | String expire() default ''; 18 | } 19 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/MemoizeHash.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis 2 | 3 | import java.lang.annotation.ElementType 4 | import java.lang.annotation.Retention 5 | import java.lang.annotation.RetentionPolicy 6 | import java.lang.annotation.Target 7 | import org.codehaus.groovy.transform.GroovyASTTransformationClass 8 | 9 | /** 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target([ElementType.METHOD]) 13 | @GroovyASTTransformationClass(['grails.plugins.redis.ast.MemoizeHashASTTransformation']) 14 | @interface MemoizeHash { 15 | Class value() default {true}; 16 | String key() default ''; 17 | String expire() default ''; 18 | } 19 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/MemoizeHashField.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis 2 | 3 | import java.lang.annotation.ElementType 4 | import java.lang.annotation.Retention 5 | import java.lang.annotation.RetentionPolicy 6 | import java.lang.annotation.Target 7 | import org.codehaus.groovy.transform.GroovyASTTransformationClass 8 | 9 | /** 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target([ElementType.METHOD]) 13 | @GroovyASTTransformationClass(['grails.plugins.redis.ast.MemoizeHashFieldASTTransformation']) 14 | @interface MemoizeHashField { 15 | String key() default ''; 16 | String member() default ''; 17 | String expire() default ''; 18 | } 19 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/MemoizeList.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis 2 | 3 | import java.lang.annotation.ElementType 4 | import java.lang.annotation.Retention 5 | import java.lang.annotation.RetentionPolicy 6 | import java.lang.annotation.Target 7 | import org.codehaus.groovy.transform.GroovyASTTransformationClass 8 | 9 | /** 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target([ElementType.METHOD]) 13 | @GroovyASTTransformationClass(['grails.plugins.redis.ast.MemoizeListASTTransformation']) 14 | @interface MemoizeList { 15 | Class value() default {true}; 16 | String key() default ''; 17 | String expire() default ''; 18 | } 19 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/MemoizeObject.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis 2 | 3 | import java.lang.annotation.ElementType 4 | import java.lang.annotation.Retention 5 | import java.lang.annotation.RetentionPolicy 6 | import java.lang.annotation.Target 7 | 8 | import org.codehaus.groovy.transform.GroovyASTTransformationClass 9 | 10 | /** 11 | */ 12 | @Retention(RetentionPolicy.SOURCE) 13 | @Target([ElementType.METHOD]) 14 | @GroovyASTTransformationClass(['grails.plugins.redis.ast.MemoizeObjectASTTransformation']) 15 | @interface MemoizeObject { 16 | Class clazz() default {}; 17 | String key() default ''; 18 | String expire() default ''; 19 | } 20 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/MemoizeScore.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis 2 | 3 | import java.lang.annotation.ElementType 4 | import java.lang.annotation.Retention 5 | import java.lang.annotation.RetentionPolicy 6 | import java.lang.annotation.Target 7 | import org.codehaus.groovy.transform.GroovyASTTransformationClass 8 | 9 | /** 10 | */ 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target([ElementType.METHOD]) 13 | @GroovyASTTransformationClass(['grails.plugins.redis.ast.MemoizeScoreASTTransformation']) 14 | @interface MemoizeScore { 15 | String key() default ''; 16 | String member() default ''; 17 | String expire() default ''; 18 | } 19 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/RedisService.groovy: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2011 SpringSource 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package grails.plugins.redis 16 | 17 | import com.google.gson.Gson 18 | import grails.core.GrailsApplication 19 | import grails.gorm.transactions.Transactional 20 | import groovy.util.logging.Slf4j 21 | import redis.clients.jedis.Jedis 22 | import redis.clients.jedis.Pipeline 23 | import redis.clients.jedis.Transaction 24 | import redis.clients.jedis.exceptions.JedisConnectionException 25 | 26 | @Slf4j 27 | class RedisService { 28 | 29 | public static final int NO_EXPIRATION_TTL = -1 30 | public static final int KEY_DOES_NOT_EXIST = -2 // added in redis 2.8 31 | 32 | def redisPool 33 | GrailsApplication grailsApplication 34 | 35 | boolean transactional = false 36 | 37 | RedisService withConnection(String connectionName) { 38 | if (grailsApplication.mainContext.containsBean("redisService${connectionName.capitalize()}")) { 39 | return (RedisService) grailsApplication.mainContext.getBean("redisService${connectionName.capitalize()}") 40 | } 41 | log.error('Connection with name redisService{} could not be found, returning default redis instead', connectionName.capitalize()) 42 | return this 43 | } 44 | 45 | def withPipeline(Closure closure, Boolean returnAll = false) { 46 | withRedis { Jedis redis -> 47 | Pipeline pipeline = redis.pipelined() 48 | closure(pipeline) 49 | returnAll ? pipeline.syncAndReturnAll() : pipeline.sync() 50 | } 51 | } 52 | 53 | def withOptionalPipeline(Closure clos, Boolean returnAll = false) { 54 | withOptionalRedis { Jedis redis -> 55 | if (redis) { 56 | Pipeline pipeline = redis.pipelined() 57 | clos(pipeline) 58 | returnAll ? pipeline.syncAndReturnAll() : pipeline.sync() 59 | } else { 60 | return clos() 61 | } 62 | 63 | } 64 | } 65 | 66 | def withTransaction(Closure closure) { 67 | withRedis { Jedis redis -> 68 | Transaction transaction = redis.multi() 69 | try { 70 | closure(transaction) 71 | } catch (Exception exception) { 72 | transaction.discard() 73 | throw exception 74 | } 75 | 76 | transaction.exec() 77 | } 78 | } 79 | 80 | def methodMissing(String name, args) { 81 | log.debug('methodMissing {}', name) 82 | withRedis { Jedis redis -> 83 | redis.invokeMethod(name, args) 84 | } 85 | } 86 | 87 | void propertyMissing(String name, Object value) { 88 | withRedis { Jedis redis -> 89 | redis.set(name, value.toString()) 90 | } 91 | } 92 | 93 | Object propertyMissing(String name) { 94 | withRedis { Jedis redis -> 95 | redis.get(name) 96 | } 97 | } 98 | 99 | def withRedis(Closure closure) { 100 | Jedis redis = redisPool.resource 101 | try { 102 | return closure(redis) 103 | } catch (JedisConnectionException jce) { 104 | throw jce 105 | } catch (Exception e) { 106 | throw e 107 | } finally { 108 | if (redis) { 109 | redis.close() 110 | } 111 | } 112 | } 113 | 114 | /** 115 | * An implementation of withRedis that suppresses JedisConnectException to support the memoization model 116 | * @param clos 117 | * @return 118 | */ 119 | def withOptionalRedis(Closure clos) { 120 | Jedis redis 121 | try { 122 | redis = redisPool.resource 123 | } 124 | catch (JedisConnectionException jce) { 125 | log.info('Unreachable redis store trying to retrieve redis resource. Please check redis server and/or config!') 126 | } 127 | 128 | try { 129 | return clos(redis) 130 | } catch (JedisConnectionException jce) { 131 | log.error('Unreachable redis store trying to return redis pool resource. Please check redis server and/or config!', jce) 132 | } catch (Throwable t) { 133 | throw t 134 | } finally { 135 | if (redis) { 136 | redis.close() 137 | } 138 | } 139 | } 140 | 141 | def memoize(String key, Integer expire, Closure closure) { 142 | memoize(key, [expire: expire], closure) 143 | } 144 | 145 | // SET/GET a value on a Redis key 146 | def memoize(String key, Map options = [:], Closure closure) { 147 | log.debug('using key {}', key) 148 | def result = withOptionalRedis { Jedis redis -> 149 | if (redis) return redis.get(key) 150 | } 151 | 152 | if (!result) { 153 | log.debug('cache miss: {}', key) 154 | result = closure() 155 | if (result) withOptionalRedis { Jedis redis -> 156 | if (redis) { 157 | if (options?.expire) { 158 | redis.setex(key, options.expire, result as String) 159 | } else { 160 | redis.set(key, result as String) 161 | } 162 | } 163 | } 164 | } else { 165 | log.debug('cache hit : {} = {}', key, result) 166 | } 167 | result 168 | } 169 | 170 | def memoizeHash(String key, Integer expire, Closure closure) { 171 | memoizeHash(key, [expire: expire], closure) 172 | } 173 | 174 | def memoizeHash(String key, Map options = [:], Closure closure) { 175 | def hash = withOptionalRedis { Jedis redis -> 176 | if (redis) return redis.hgetAll(key) 177 | } 178 | 179 | if (!hash) { 180 | log.debug('cache miss: {}', key) 181 | hash = closure() 182 | if (hash) withOptionalRedis { Jedis redis -> 183 | if (redis) { 184 | redis.hmset(key, hash) 185 | if (options?.expire) redis.expire(key, options.expire) 186 | } 187 | } 188 | } else { 189 | log.debug('cache hit : {} = {}', key, hash) 190 | } 191 | hash 192 | } 193 | 194 | def memoizeHashField(String key, String field, Integer expire, Closure closure) { 195 | memoizeHashField(key, field, [expire: expire], closure) 196 | } 197 | 198 | // HSET/HGET a value on a Redis hash at key.field 199 | // if expire is not null it will be the expire for the whole hash, not this value 200 | // and will only be set if there isn't already a TTL on the hash 201 | def memoizeHashField(String key, String field, Map options = [:], Closure closure) { 202 | def result = withOptionalRedis { Jedis redis -> 203 | if (redis) return redis.hget(key, field) 204 | } 205 | 206 | if (!result) { 207 | log.debug('cache miss: {}.{}', key, field) 208 | result = closure() 209 | if (result) withOptionalRedis { Jedis redis -> 210 | if (redis) { 211 | redis.hset(key, field, result as String) 212 | if (options?.expire && redis.ttl(key) == NO_EXPIRATION_TTL) redis.expire(key, options.expire) 213 | } 214 | } 215 | } else { 216 | log.debug('cache hit : {}.{} = {}', key, field, result) 217 | } 218 | result 219 | } 220 | 221 | def memoizeScore(String key, String member, Integer expire, Closure closure) { 222 | memoizeScore(key, member, [expire: expire], closure) 223 | } 224 | 225 | // set/get a 'double' score within a sorted set 226 | // if expire is not null it will be the expire for the whole zset, not this value 227 | // and will only be set if there isn't already a TTL on the zset 228 | def memoizeScore(String key, String member, Map options = [:], Closure closure) { 229 | def score = withOptionalRedis { Jedis redis -> 230 | if (redis) redis.zscore(key, member) 231 | } 232 | 233 | if (!score) { 234 | log.debug('cache miss: {}.{}', key, member) 235 | score = closure() 236 | if (score) withOptionalRedis { Jedis redis -> 237 | if (redis) { 238 | redis.zadd(key, score, member) 239 | if (options?.expire && redis.ttl(key) == NO_EXPIRATION_TTL) redis.expire(key, options.expire) 240 | } 241 | } 242 | } else { 243 | log.debug('cache hit : {}.{} = {}', key, member, score) 244 | } 245 | score 246 | } 247 | 248 | List memoizeDomainList(Class domainClass, String key, Integer expire, Closure closure) { 249 | memoizeDomainList(domainClass, key, [expire: expire], closure) 250 | } 251 | 252 | List memoizeDomainList(Class domainClass, String key, Map options = [:], Closure closure) { 253 | List idList = getIdListFor(key) 254 | if (idList) return hydrateDomainObjectsFrom(domainClass, idList) 255 | 256 | def domainList = withOptionalRedis { Jedis redis -> 257 | closure(redis) 258 | } 259 | 260 | saveIdListTo(key, domainList, options.expire) 261 | 262 | domainList 263 | } 264 | 265 | List memoizeDomainIdList(Class domainClass, String key, Integer expire, Closure closure) { 266 | memoizeDomainIdList(domainClass, key, [expire: expire], closure) 267 | } 268 | 269 | // used when we just want the list of Ids back rather than hydrated objects 270 | List memoizeDomainIdList(Class domainClass, String key, Map options = [:], Closure closure) { 271 | List idList = getIdListFor(key) 272 | if (idList) return idList 273 | 274 | def domainList = closure() 275 | 276 | saveIdListTo(key, domainList, options.expire) 277 | 278 | getIdListFor(key) 279 | } 280 | 281 | protected List getIdListFor(String key) { 282 | List idList = withOptionalRedis { Jedis redis -> 283 | if (redis) return redis.lrange(key, 0, -1) 284 | } 285 | 286 | if (idList) { 287 | log.debug('{} cache hit, returning {} ids', key, idList.size()) 288 | List idLongList = idList*.toLong() 289 | return idLongList 290 | } 291 | } 292 | 293 | protected void saveIdListTo(String key, List domainList, Integer expire = null) { 294 | log.debug('{} cache miss, memoizing {} ids', key, domainList?.size() ?: 0) 295 | withOptionalPipeline { pipeline -> 296 | if (pipeline) { 297 | for (domain in domainList) { 298 | pipeline.rpush(key, domain.id as String) 299 | } 300 | if (expire) pipeline.expire(key, expire) 301 | } 302 | } 303 | } 304 | 305 | @Transactional(readOnly = true) 306 | protected List hydrateDomainObjectsFrom(Class domainClass, List idList) { 307 | if (domainClass && idList) { 308 | //return domainClass.findAllByIdInList(idList, [cache: true]) 309 | return idList.collect { id -> domainClass.load(id) } 310 | } 311 | [] 312 | } 313 | 314 | def memoizeDomainObject(Class domainClass, String key, Integer expire, Closure closure) { 315 | memoizeDomainObject(domainClass, key, [expire: expire], closure) 316 | } 317 | 318 | // closure can return either a domain object or a Long id of a domain object 319 | // it will be persisted into redis as the Long 320 | @Transactional(readOnly = true) 321 | def memoizeDomainObject(Class domainClass, String key, Map options = [:], Closure closure) { 322 | Long domainId = withOptionalRedis { redis -> 323 | redis?.get(key)?.toLong() 324 | } 325 | if (!domainId) domainId = persistDomainId(closure()?.id as Long, key, options.expire) 326 | domainClass.load(domainId) 327 | } 328 | 329 | Long persistDomainId(Long domainId, String key, Integer expire) { 330 | if (domainId) { 331 | withOptionalPipeline { pipeline -> 332 | if (pipeline) { 333 | pipeline.set(key, domainId.toString()) 334 | if (expire) pipeline.expire(key, expire) 335 | } 336 | } 337 | } 338 | domainId 339 | } 340 | 341 | def memoizeObject(Class clazz, String key, Integer expire, Closure closure) { 342 | memoizeObject(clazz, key, [expire: expire], closure) 343 | } 344 | 345 | def memoizeObject(Class clazz, String key, Map options = [:], Closure closure) { 346 | Gson gson = new Gson() 347 | 348 | String memoizedJson = memoize(key, options) { -> 349 | def original = closure() 350 | if (original == null && options.cacheNull == false) return null 351 | gson.toJson(original) 352 | } 353 | 354 | gson.fromJson((String) memoizedJson, clazz) 355 | } 356 | 357 | // deletes all keys matching a pattern (see redis "keys" documentation for more) 358 | // OK for low traffic methods, but expensive compared to other redis commands 359 | // perf test before relying on this rather than storing your own set of keys to 360 | // delete 361 | void deleteKeysWithPattern(keyPattern) { 362 | log.info('Cleaning all redis keys with pattern [{}]', keyPattern.toString()) 363 | withRedis { Jedis redis -> 364 | String[] keys = redis.keys(keyPattern) 365 | if (keys) redis.del(keys) 366 | } 367 | } 368 | 369 | /** 370 | * Deletes key from redis. 371 | * 372 | * @param key The key to delete. 373 | */ 374 | void deleteKey(String key){ 375 | withRedis { Jedis redis -> 376 | redis.del(key) 377 | } 378 | } 379 | 380 | def memoizeList(String key, Integer expire, Closure closure) { 381 | memoizeList(key, [expire: expire], closure) 382 | } 383 | 384 | def memoizeList(String key, Map options = [:], Closure closure) { 385 | List list = withOptionalRedis { Jedis redis -> 386 | if (redis) return redis.lrange(key, 0, -1) 387 | } 388 | 389 | if (!list) { 390 | log.debug('cache miss: {}', key) 391 | list = closure() 392 | if (list) withOptionalPipeline { pipeline -> 393 | if (pipeline) { 394 | for (obj in list) { 395 | pipeline.rpush(key, obj) 396 | } 397 | if (options?.expire) pipeline.expire(key, options.expire) 398 | } 399 | } 400 | } else { 401 | log.debug('cache hit: {}', key) 402 | } 403 | list 404 | } 405 | 406 | def memoizeSet(String key, Integer expire, Closure closure) { 407 | memoizeSet(key, [expire: expire], closure) 408 | } 409 | 410 | def memoizeSet(String key, Map options = [:], Closure closure) { 411 | def set = withOptionalRedis { Jedis redis -> 412 | if (redis) return redis.smembers(key) 413 | } 414 | 415 | if (!set) { 416 | log.debug('cache miss: {}', key) 417 | set = closure() 418 | if (set) withOptionalPipeline { pipeline -> 419 | if (pipeline) { 420 | for (obj in set) { 421 | pipeline.sadd(key, obj) 422 | } 423 | if (options?.expire) pipeline.expire(key, options.expire) 424 | } 425 | } 426 | } else { 427 | log.debug('cache hit: {}', key) 428 | } 429 | set 430 | } 431 | // should ONLY Be used from tests unless we have a really good reason to clear out the entire redis db 432 | def flushDB() { 433 | log.warn('flushDB called!') 434 | withRedis { Jedis redis -> 435 | redis.flushDB() 436 | } 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/ast/AbstractMemoizeASTTransformation.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis.ast 2 | 3 | import grails.plugins.redis.RedisService 4 | import grails.util.Holders 5 | import org.codehaus.groovy.ast.* 6 | import org.codehaus.groovy.ast.builder.AstBuilder 7 | import org.codehaus.groovy.ast.expr.* 8 | import org.codehaus.groovy.ast.stmt.BlockStatement 9 | import org.codehaus.groovy.ast.stmt.ReturnStatement 10 | import org.codehaus.groovy.ast.stmt.Statement 11 | import org.codehaus.groovy.classgen.VariableScopeVisitor 12 | import org.codehaus.groovy.control.CompilePhase 13 | import org.codehaus.groovy.control.SourceUnit 14 | import org.codehaus.groovy.control.messages.SyntaxErrorMessage 15 | import org.codehaus.groovy.syntax.SyntaxException 16 | import org.codehaus.groovy.transform.ASTTransformation 17 | import org.codehaus.groovy.transform.GroovyASTTransformation 18 | import org.springframework.beans.factory.annotation.Autowired 19 | 20 | import static org.springframework.asm.Opcodes.ACC_PUBLIC 21 | import static org.springframework.asm.Opcodes.ACC_STATIC 22 | 23 | /** 24 | */ 25 | @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) 26 | abstract class AbstractMemoizeASTTransformation implements ASTTransformation { 27 | 28 | protected static final String KEY = 'key' 29 | protected static final String MEMOIZE_KEY = 'memKey' 30 | protected static final String EXPIRE = 'expire' 31 | protected static final String CLAZZ = 'clazz' 32 | protected static final String MEMBER = 'member' 33 | protected static final String HASH_CODE = '#' 34 | protected static final String GSTRING = '$' 35 | protected static final String REDIS_SERVICE = 'redisService' 36 | protected static final String GET_REDIS_SERVICE = 'getRedisService' 37 | protected static final String THIS = 'this' 38 | protected static final String PRINTLN = 'println' 39 | 40 | public static final ClassNode AUTOWIRED_CLASS_NODE = new ClassNode(Autowired).getPlainNodeReference() 41 | 42 | void visit(ASTNode[] astNodes, SourceUnit sourceUnit) { 43 | //return //todo: this isn't working with grails 3.0+, UGH! 44 | 45 | //map to hold the params we will pass to the memoize[?] method 46 | def memoizeProperties = [:] 47 | 48 | try { 49 | injectService(sourceUnit, REDIS_SERVICE, RedisService) 50 | generateMemoizeProperties(astNodes, sourceUnit, memoizeProperties) 51 | //if the key is missing there is an issue with the annotation 52 | if (!memoizeProperties.containsKey(KEY) || !memoizeProperties.get(KEY)) { 53 | return 54 | } 55 | addMemoizedStatements((MethodNode) astNodes[1], memoizeProperties) 56 | visitVariableScopes(sourceUnit) 57 | } catch (Exception e) { 58 | addError("Error during Memoize AST Transformation: ${e}", astNodes[0], sourceUnit) 59 | throw e 60 | } 61 | } 62 | 63 | /** 64 | * Create the statements for the memoized method, clear the node and then readd the memoized code back to the method. 65 | * @param methodNode The MethodNode we will be clearing and replacing with the redisService.memoize[?] method call with. 66 | * @param memoizeProperties The map of properties to use for the service invocation 67 | */ 68 | private void addMemoizedStatements(MethodNode methodNode, LinkedHashMap memoizeProperties) { 69 | def stmt = memoizeMethod(methodNode, memoizeProperties) 70 | methodNode.code.statements.clear() 71 | methodNode.code.statements.addAll(stmt) 72 | } 73 | 74 | /** 75 | * Fix the variable scopes for closures. Without this closures will be missing the input params being passed from the parent scope. 76 | * @param sourceUnit The SourceUnit to visit and add the variable scopes. 77 | */ 78 | private static void visitVariableScopes(SourceUnit sourceUnit) { 79 | VariableScopeVisitor scopeVisitor = new VariableScopeVisitor(sourceUnit); 80 | sourceUnit.AST.classes.each { 81 | scopeVisitor.visitClass(it) 82 | } 83 | } 84 | 85 | /** 86 | * Determine if the user missed injecting the redisService into the class with the @Memoized method. 87 | * @param sourceUnit SourceUnit to detect and/or inject service into 88 | * @param serviceName name of the service to detect and/or inject 89 | * @param serviceClass Class of the service 90 | */ 91 | protected static void injectService(SourceUnit sourceUnit, String serviceName, Class serviceClass) { 92 | def classNode = (ClassNode) sourceUnit.AST.classes.toArray()[0] 93 | if (!classNode.fields?.any { it.name == serviceName }) { 94 | addImport(sourceUnit, serviceClass) 95 | addImport(sourceUnit, Holders) 96 | addStarImport(sourceUnit, Holders) 97 | addRedisServiceBuilder(classNode, serviceName, RedisService) 98 | addFieldToTransients(classNode, serviceName) 99 | } 100 | } 101 | 102 | 103 | private static void addStarImport(SourceUnit sourceUnit, Class serviceClass) { 104 | if (!sourceUnit.AST.starImports.any { it.packageName =~ "${ClassHelper.make(serviceClass).packageName}" }) { 105 | sourceUnit.AST.addStarImport(ClassHelper.make(serviceClass).packageName) 106 | } 107 | } 108 | 109 | private static void addImport(SourceUnit sourceUnit, Class serviceClass) { 110 | if (!sourceUnit.AST.imports.any { it.className == ClassHelper.make(serviceClass).name } 111 | && !sourceUnit.AST.starImports.any { 112 | it.packageName == "${ClassHelper.make(serviceClass).packageName}.".toString() 113 | }) { 114 | sourceUnit.AST.addImport(serviceClass.simpleName, ClassHelper.make(serviceClass)) 115 | } 116 | } 117 | 118 | private static void addRedisService(ClassNode cNode, 119 | String propertyName, 120 | Class propertyType = Object.class, 121 | Expression initialValue = null) { 122 | //def ast = new AstBuilder().buildFromString("Holders?.findApplicationContext()") 123 | //?.getBean('redisService')") 124 | 125 | FieldNode fieldNode = new FieldNode( 126 | propertyName, 127 | ACC_PUBLIC, 128 | new ClassNode(propertyType), 129 | new ClassNode(cNode.class), 130 | initialValue 131 | ) 132 | 133 | def holderExpression = new StaticMethodCallExpression(ClassHelper.make(Holders.class), 'findApplicationContext', null) 134 | ArgumentListExpression argumentListExpression = new ArgumentListExpression() 135 | argumentListExpression.addExpression(makeConstantExpression('redisService')) 136 | def getBeanExpression = new MethodCallExpression(holderExpression, 'getBean', argumentListExpression) 137 | getBeanExpression.setSafe(true) 138 | 139 | def returnStatement = new ReturnStatement(getBeanExpression) 140 | 141 | 142 | def propertyNode = new PropertyNode(fieldNode, ACC_PUBLIC, returnStatement, null) 143 | cNode.addProperty(propertyNode) 144 | } 145 | 146 | private static void addRedisServiceBuilder(ClassNode cNode, String propertyName, 147 | Class propertyType = Object.class, 148 | Expression initialValue = null) { 149 | def ast = new AstBuilder().buildFromString("return Holders?.findApplicationContext()?.getBean('redisService')") 150 | 151 | FieldNode fieldNode = new FieldNode( 152 | propertyName, 153 | ACC_PUBLIC, 154 | new ClassNode(propertyType), 155 | new ClassNode(cNode.class), 156 | initialValue 157 | ) 158 | 159 | def returnStatement = new ReturnStatement(ast[0].statements[0].expression) 160 | 161 | def propertyNode = new PropertyNode(fieldNode, ACC_PUBLIC, returnStatement, null) 162 | cNode.addProperty(propertyNode) 163 | 164 | } 165 | 166 | private static void addFieldToTransients(ClassNode parentClass, String propertyName) { 167 | if (parentClass != null) { 168 | FieldNode transients = parentClass.getField("transients") 169 | if (!transients) { 170 | transients = parentClass.addField( 171 | "transients", 172 | ACC_PUBLIC | ACC_STATIC, 173 | ClassHelper.DYNAMIC_TYPE, 174 | new ListExpression() 175 | ) 176 | } 177 | ((ListExpression) transients.initialExpression).addExpression(new ConstantExpression(propertyName)) 178 | ((ListExpression) transients.initialExpression).addExpression(new ConstantExpression("get${propertyName.capitalize()}")) 179 | } 180 | } 181 | 182 | /** 183 | * This method adds a new property to the class. Groovy automatically handles adding the getters and setters so you 184 | * don't have to create special methods for those. This could be reused for other properties. 185 | * @param cNode Node to inject property onto. Usually a ClassNode for the current class. 186 | * @param anotationName The name of the property to inject. 187 | * @param propertyType The object class of the property. (defaults to Object.class) 188 | * @param initialValue Initial value of the property. (defaults null) 189 | */ 190 | private static void addAutowiredAnnotation(AnnotatedNode node) { 191 | final AnnotationNode autowiredAnnotation = new AnnotationNode(AUTOWIRED_CLASS_NODE) 192 | node.addAnnotation(autowiredAnnotation) 193 | } 194 | 195 | /** 196 | * method to add the key and expires and options if they exist 197 | * @param astNodes the ast nodes 198 | * @param sourceUnit the source unit 199 | * @param memoizeProperties map to put data in 200 | * @return 201 | */ 202 | protected abstract void generateMemoizeProperties(ASTNode[] astNodes, SourceUnit sourceUnit, Map memoizeProperties) 203 | 204 | protected abstract ConstantExpression makeRedisServiceConstantExpression() 205 | 206 | protected abstract ArgumentListExpression makeRedisServiceArgumentListExpression(Map memoizeProperties) 207 | 208 | protected List memoizeMethod(MethodNode methodNode, Map memoizeProperties) { 209 | BlockStatement body = new BlockStatement() 210 | addRedisServiceMemoizeInvocation(body, methodNode, memoizeProperties) 211 | body.statements 212 | } 213 | 214 | protected void addRedisServiceMemoizeInvocation(BlockStatement body, MethodNode methodNode, Map memoizeProperties) { 215 | ArgumentListExpression argumentListExpression = makeRedisServiceArgumentListExpression(memoizeProperties) 216 | argumentListExpression.addExpression(makeClosureExpression(methodNode)) 217 | 218 | def ast = new AstBuilder().buildFromString("getRedisService()") 219 | 220 | def getRedisServiceMethodExpression = ast[0].statements[0].expression as MethodCallExpression 221 | getRedisServiceMethodExpression.setSafe(true) 222 | 223 | def redisServiceMethodMethodExpression = new MethodCallExpression( 224 | getRedisServiceMethodExpression, 225 | makeRedisServiceConstantExpression(), 226 | argumentListExpression 227 | ) 228 | 229 | redisServiceMethodMethodExpression.setSafe(true) 230 | 231 | body.addStatement( 232 | new ReturnStatement( 233 | redisServiceMethodMethodExpression 234 | ) 235 | ) 236 | } 237 | 238 | protected 239 | static void addRedisServiceMemoizeKeyExpression(Map memoizeProperties, ArgumentListExpression argumentListExpression) { 240 | if (memoizeProperties.get(KEY).toString().contains(HASH_CODE)) { 241 | def ast = new AstBuilder().buildFromString(""" 242 | "${memoizeProperties.get(KEY).toString().replace(HASH_CODE, GSTRING).toString()}" 243 | """) 244 | argumentListExpression.addExpression(ast[0].statements[0].expression) 245 | } else { 246 | argumentListExpression.addExpression(new ConstantExpression(memoizeProperties.get(KEY).toString())) 247 | } 248 | } 249 | 250 | protected 251 | static void addRedisServiceMemoizeExpireExpression(Map memoizeProperties, ArgumentListExpression argumentListExpression) { 252 | if (memoizeProperties.get(EXPIRE).toString().contains(HASH_CODE)) { 253 | def ast = new AstBuilder().buildFromString(""" 254 | Integer.parseInt("${memoizeProperties.get(EXPIRE).toString().replace(HASH_CODE, GSTRING).toString()}") 255 | """) 256 | 257 | argumentListExpression.addExpression(ast[0].statements[0].expression) 258 | } else { 259 | argumentListExpression.addExpression(makeConstantExpression(Integer.parseInt(memoizeProperties.get(EXPIRE).toString()))) 260 | } 261 | } 262 | 263 | protected static ClosureExpression makeClosureExpression(MethodNode methodNode) { 264 | ClosureExpression closureExpression = new ClosureExpression( 265 | [] as Parameter[], 266 | new BlockStatement(methodNode.code.statements as Statement[], new VariableScope()) 267 | ) 268 | closureExpression.variableScope = new VariableScope() 269 | closureExpression 270 | } 271 | 272 | protected static ConstantExpression makeConstantExpression(constantExpression) { 273 | new ConstantExpression(constantExpression) 274 | } 275 | 276 | protected static void addError(String msg, ASTNode node, SourceUnit source) { 277 | int line = node.lineNumber 278 | int col = node.columnNumber 279 | SyntaxException se = new SyntaxException("${msg}\n", line, col) 280 | SyntaxErrorMessage sem = new SyntaxErrorMessage(se, source) 281 | source.errorCollector.addErrorAndContinue(sem) 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/ast/MemoizeASTTransformation.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis.ast 2 | 3 | import org.codehaus.groovy.ast.ASTNode 4 | import org.codehaus.groovy.ast.expr.ArgumentListExpression 5 | import org.codehaus.groovy.ast.expr.ClosureExpression 6 | import org.codehaus.groovy.ast.expr.ConstantExpression 7 | import org.codehaus.groovy.control.CompilePhase 8 | import org.codehaus.groovy.control.SourceUnit 9 | import org.codehaus.groovy.transform.GroovyASTTransformation 10 | 11 | @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) 12 | class MemoizeASTTransformation extends AbstractMemoizeASTTransformation { 13 | 14 | @Override 15 | protected void generateMemoizeProperties(ASTNode[] astNodes, SourceUnit sourceUnit, Map memoizeProperties) { 16 | def expire = astNodes[0]?.members?.expire?.text 17 | def keyString = astNodes[0]?.members?.key?.text 18 | def keyClosure = astNodes[0]?.members?.value 19 | 20 | if(!validateMemoizeProperties(keyClosure, keyString, astNodes, sourceUnit, expire)) { 21 | return 22 | } 23 | //*************************************************************************** 24 | 25 | memoizeProperties.put(KEY, (keyClosure) ? keyClosure?.code?.statements[0]?.expression?.value : keyString) 26 | if(expire) { 27 | memoizeProperties.put(EXPIRE, expire) 28 | } 29 | } 30 | 31 | private static Boolean validateMemoizeProperties(keyClosure, keyString, ASTNode[] astNodes, SourceUnit sourceUnit, expire) { 32 | if(keyClosure?.class != ClosureExpression && keyString.class != String) { 33 | addError('Internal Error: annotation does not contain key closure or key property', astNodes[0], sourceUnit) 34 | return false 35 | } 36 | 37 | if(keyClosure && keyClosure.code?.statements[0]?.expression?.value?.class != String) { 38 | addError('Internal Error: annotation does not contain string key closure', astNodes[0], sourceUnit) 39 | return false 40 | } 41 | 42 | if(expire && expire.class != String && !Integer.parseInt(expire)) { 43 | addError('Internal Error: provided expire is not an String (in millis)', astNodes[0], sourceUnit) 44 | return false 45 | } 46 | true 47 | } 48 | 49 | @Override 50 | protected ConstantExpression makeRedisServiceConstantExpression() { 51 | new ConstantExpression('memoize') 52 | } 53 | 54 | @Override 55 | protected ArgumentListExpression makeRedisServiceArgumentListExpression(Map memoizeProperties) { 56 | ArgumentListExpression argumentListExpression = new ArgumentListExpression() 57 | addRedisServiceMemoizeKeyExpression(memoizeProperties, argumentListExpression) 58 | if(memoizeProperties.containsKey(MEMBER)) { 59 | argumentListExpression.addExpression(makeConstantExpression(memoizeProperties.get(MEMBER).toString())) 60 | } 61 | if(memoizeProperties.containsKey(EXPIRE)) { 62 | addRedisServiceMemoizeExpireExpression(memoizeProperties, argumentListExpression) 63 | } 64 | argumentListExpression 65 | } 66 | } -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/ast/MemoizeDomainListASTTransformation.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis.ast 2 | 3 | import org.codehaus.groovy.ast.expr.ConstantExpression 4 | import org.codehaus.groovy.control.CompilePhase 5 | import org.codehaus.groovy.transform.GroovyASTTransformation 6 | 7 | @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) 8 | class MemoizeDomainListASTTransformation extends MemoizeDomainObjectASTTransformation { 9 | 10 | @Override 11 | protected ConstantExpression makeRedisServiceConstantExpression() { 12 | new ConstantExpression('memoizeDomainList') 13 | } 14 | } -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/ast/MemoizeDomainObjectASTTransformation.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis.ast 2 | 3 | import org.codehaus.groovy.ast.ASTNode 4 | import org.codehaus.groovy.ast.expr.ArgumentListExpression 5 | import org.codehaus.groovy.ast.expr.ClassExpression 6 | import org.codehaus.groovy.ast.expr.ConstantExpression 7 | import org.codehaus.groovy.ast.expr.Expression 8 | import org.codehaus.groovy.control.CompilePhase 9 | import org.codehaus.groovy.control.SourceUnit 10 | import org.codehaus.groovy.transform.GroovyASTTransformation 11 | 12 | @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) 13 | class MemoizeDomainObjectASTTransformation extends AbstractMemoizeASTTransformation { 14 | 15 | @Override 16 | protected void generateMemoizeProperties(ASTNode[] astNodes, SourceUnit sourceUnit, Map memoizeProperties) { 17 | def keyString = astNodes[0]?.members?.key?.text 18 | def clazz = astNodes[0]?.members?.clazz 19 | def expire = astNodes[0]?.members?.expire?.text 20 | 21 | if(!validateMemoizeProperties(clazz, astNodes, sourceUnit, keyString, expire)) { 22 | return 23 | } 24 | 25 | //*************************************************************************** 26 | 27 | memoizeProperties.put(KEY, keyString) 28 | memoizeProperties.put(CLAZZ, clazz) 29 | if(expire) { 30 | memoizeProperties.put(EXPIRE, expire) 31 | } 32 | } 33 | 34 | private Boolean validateMemoizeProperties(clazz, ASTNode[] astNodes, SourceUnit sourceUnit, keyString, expire) { 35 | if(!clazz?.class == ClassExpression) { 36 | addError('Internal Error: annotation does not contain clazz property', astNodes[0], sourceUnit) 37 | return false 38 | } 39 | 40 | if(keyString?.class != String) { 41 | addError('Internal Error: annotation does not contain key String', astNodes[0], sourceUnit) 42 | return false 43 | } 44 | 45 | if(expire && expire.class != String && !Integer.parseInt(expire)) { 46 | addError('Internal Error: provided expire is not an String (in millis)', astNodes[0], sourceUnit) 47 | return false 48 | } 49 | true 50 | } 51 | 52 | @Override 53 | protected ConstantExpression makeRedisServiceConstantExpression() { 54 | new ConstantExpression('memoizeDomainObject') 55 | } 56 | 57 | @Override 58 | protected ArgumentListExpression makeRedisServiceArgumentListExpression(Map memoizeProperties) { 59 | ArgumentListExpression argumentListExpression = new ArgumentListExpression() 60 | argumentListExpression.addExpression((Expression) memoizeProperties.get(CLAZZ)) 61 | addRedisServiceMemoizeKeyExpression(memoizeProperties, argumentListExpression) 62 | if(memoizeProperties.containsKey(EXPIRE)) { 63 | addRedisServiceMemoizeExpireExpression(memoizeProperties, argumentListExpression) 64 | } 65 | argumentListExpression 66 | } 67 | } -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/ast/MemoizeHashASTTransformation.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis.ast 2 | 3 | import org.codehaus.groovy.ast.expr.ConstantExpression 4 | import org.codehaus.groovy.control.CompilePhase 5 | import org.codehaus.groovy.transform.GroovyASTTransformation 6 | 7 | @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) 8 | class MemoizeHashASTTransformation extends MemoizeASTTransformation { 9 | 10 | @Override 11 | protected ConstantExpression makeRedisServiceConstantExpression() { 12 | new ConstantExpression('memoizeHash') 13 | } 14 | } -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/ast/MemoizeHashFieldASTTransformation.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis.ast 2 | 3 | import org.codehaus.groovy.ast.ASTNode 4 | import org.codehaus.groovy.ast.expr.ConstantExpression 5 | import org.codehaus.groovy.control.CompilePhase 6 | import org.codehaus.groovy.control.SourceUnit 7 | import org.codehaus.groovy.transform.GroovyASTTransformation 8 | 9 | @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) 10 | class MemoizeHashFieldASTTransformation extends MemoizeASTTransformation { 11 | 12 | @Override 13 | protected void generateMemoizeProperties(ASTNode[] astNodes, SourceUnit sourceUnit, Map memoizeProperties) { 14 | super.generateMemoizeProperties(astNodes, sourceUnit, memoizeProperties) 15 | def member = astNodes[0]?.members?.member?.value 16 | 17 | if(!member || member?.class != String) { 18 | addError('Internal Error: member is required for score', astNodes[0], sourceUnit) 19 | return 20 | } 21 | 22 | memoizeProperties.put(MEMBER, member) 23 | } 24 | 25 | @Override 26 | protected ConstantExpression makeRedisServiceConstantExpression() { 27 | new ConstantExpression('memoizeHashField') 28 | } 29 | } -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/ast/MemoizeListASTTransformation.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis.ast 2 | 3 | import org.codehaus.groovy.ast.expr.ConstantExpression 4 | import org.codehaus.groovy.control.CompilePhase 5 | import org.codehaus.groovy.transform.GroovyASTTransformation 6 | 7 | @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) 8 | class MemoizeListASTTransformation extends MemoizeASTTransformation { 9 | 10 | @Override 11 | protected ConstantExpression makeRedisServiceConstantExpression() { 12 | new ConstantExpression('memoizeList') 13 | } 14 | } -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/ast/MemoizeObjectASTTransformation.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis.ast 2 | 3 | import org.codehaus.groovy.ast.ASTNode 4 | import org.codehaus.groovy.ast.ClassHelper; 5 | import org.codehaus.groovy.ast.ClassNode 6 | import org.codehaus.groovy.ast.MethodNode 7 | import org.codehaus.groovy.ast.VariableScope; 8 | import org.codehaus.groovy.ast.expr.ArgumentListExpression 9 | import org.codehaus.groovy.ast.expr.ClassExpression; 10 | import org.codehaus.groovy.ast.expr.ConstantExpression 11 | import org.codehaus.groovy.ast.expr.ConstructorCallExpression; 12 | import org.codehaus.groovy.ast.expr.Expression; 13 | import org.codehaus.groovy.ast.expr.MethodCallExpression 14 | import org.codehaus.groovy.control.CompilePhase 15 | import org.codehaus.groovy.control.SourceUnit 16 | import org.codehaus.groovy.ast.stmt.BlockStatement 17 | import org.codehaus.groovy.ast.stmt.ReturnStatement 18 | import org.codehaus.groovy.ast.stmt.Statement 19 | import org.codehaus.groovy.classgen.VariableScopeVisitor 20 | 21 | import com.google.gson.Gson 22 | 23 | import grails.plugins.redis.RedisService 24 | 25 | import org.codehaus.groovy.transform.GroovyASTTransformation 26 | 27 | @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) 28 | class MemoizeObjectASTTransformation extends AbstractMemoizeASTTransformation { 29 | 30 | @Override 31 | void visit(ASTNode[] astNodes, SourceUnit sourceUnit) { 32 | //map to hold the params we will pass to the memoize[?] method 33 | def memoizeProperties = [:] 34 | 35 | try { 36 | injectService(sourceUnit, REDIS_SERVICE, RedisService) 37 | injectImport(sourceUnit, Gson) 38 | generateMemoizeProperties(astNodes, sourceUnit, memoizeProperties) 39 | //if the key is missing there is an issue with the annotation 40 | if(!memoizeProperties.containsKey(KEY) || !memoizeProperties.get(KEY)) { 41 | return 42 | } 43 | addMemoizedStatements((MethodNode) astNodes[1], memoizeProperties) 44 | visitVariableScopes(sourceUnit) 45 | } catch (Exception e) { 46 | addError("Error during Memoize AST Transformation: ${e}", astNodes[0], sourceUnit) 47 | throw e 48 | } 49 | } 50 | 51 | /** 52 | * Create the statements for the memoized method, clear the node and then readd the memoized code back to the method. 53 | * @param methodNode The MethodNode we will be clearing and replacing with the redisService.memoize[?] method call with. 54 | * @param memoizeProperties The map of properties to use for the service invocation 55 | */ 56 | private void addMemoizedStatements(MethodNode methodNode, LinkedHashMap memoizeProperties) { 57 | def stmt = memoizeMethod(methodNode, memoizeProperties) 58 | methodNode.code.statements.clear() 59 | methodNode.code.statements.addAll(stmt) 60 | } 61 | 62 | @Override 63 | protected List memoizeMethod(MethodNode methodNode, Map memoizeProperties) { 64 | BlockStatement body = new BlockStatement() 65 | addToJson(methodNode) 66 | addRedisServiceMemoizeInvocation(body, methodNode, memoizeProperties) 67 | body = addFromJson(body, memoizeProperties) 68 | body.statements 69 | } 70 | 71 | private ConstructorCallExpression createGson(){ 72 | // new Gson() 73 | return new ConstructorCallExpression( 74 | new ClassNode(Gson.class), 75 | new ArgumentListExpression()) 76 | } 77 | 78 | private void addToJson(MethodNode methodNode){ 79 | List stmts = methodNode.code.getStatements() 80 | 81 | // new Gson().toJson(...) 82 | ReturnStatement toJsonStatment = new ReturnStatement( 83 | new MethodCallExpression( 84 | createGson(), 85 | new ConstantExpression('toJson'), 86 | new ArgumentListExpression( 87 | stmts[-1].expression 88 | ) 89 | ) 90 | ) 91 | 92 | stmts[-1] = toJsonStatment 93 | methodNode.setCode(new BlockStatement(stmts as Statement[], new VariableScope())) 94 | } 95 | 96 | private BlockStatement addFromJson(BlockStatement body, Map memoizeProperties){ 97 | // last statement should be the redisService.memoize(...){...} call 98 | List stmts = body.getStatements() 99 | 100 | ArgumentListExpression fromJsonArgList = new ArgumentListExpression() 101 | fromJsonArgList.addExpression(stmts[-1].expression) 102 | fromJsonArgList.addExpression((Expression) memoizeProperties.get(CLAZZ)) 103 | 104 | // new Gson().fromJson(..., .class) 105 | ReturnStatement fromJsonStatement = new ReturnStatement( 106 | new MethodCallExpression( 107 | createGson(), 108 | new ConstantExpression('fromJson'), 109 | fromJsonArgList 110 | ) 111 | ) 112 | stmts[-1] = fromJsonStatement 113 | new BlockStatement(stmts as Statement[], new VariableScope()) 114 | } 115 | 116 | @Override 117 | protected void generateMemoizeProperties(ASTNode[] astNodes, SourceUnit sourceUnit, Map memoizeProperties) { 118 | def expire = astNodes[0]?.members?.expire?.text 119 | def keyString = astNodes[0]?.members?.key?.text 120 | def clazz = astNodes[0]?.members?.clazz 121 | 122 | if(!validateMemoizeProperties(astNodes, sourceUnit, keyString, expire, clazz)) { 123 | return 124 | } 125 | 126 | memoizeProperties.put(KEY, keyString) 127 | memoizeProperties.put(CLAZZ, clazz) 128 | if(expire) { 129 | memoizeProperties.put(EXPIRE, expire) 130 | } 131 | } 132 | 133 | private Boolean validateMemoizeProperties(ASTNode[] astNodes, SourceUnit sourceUnit, keyString, expire, clazz) { 134 | if(keyString.class != String) { 135 | addError('Internal Error: annotation does not contain key closure or key property', astNodes[0], sourceUnit) 136 | return false 137 | } 138 | if(!clazz?.class == ClassExpression) { 139 | addError('Internal Error: annotation does not contain clazz property', astNodes[0], sourceUnit) 140 | return false 141 | } 142 | if(expire && expire.class != String && !Integer.parseInt(expire)) { 143 | addError('Internal Error: provided expire is not an String (in millis)', astNodes[0], sourceUnit) 144 | return false 145 | } 146 | true 147 | } 148 | 149 | @Override 150 | protected ConstantExpression makeRedisServiceConstantExpression() { 151 | new ConstantExpression('memoize') 152 | } 153 | 154 | @Override 155 | protected ArgumentListExpression makeRedisServiceArgumentListExpression(Map memoizeProperties) { 156 | ArgumentListExpression argumentListExpression = new ArgumentListExpression() 157 | addRedisServiceMemoizeKeyExpression(memoizeProperties, argumentListExpression) 158 | if(memoizeProperties.containsKey(EXPIRE)) { 159 | addRedisServiceMemoizeExpireExpression(memoizeProperties, argumentListExpression) 160 | } 161 | argumentListExpression 162 | } 163 | 164 | /** 165 | * Fix the variable scopes for closures. Without this closures will be missing the input params being passed from the parent scope. 166 | * @param sourceUnit The SourceUnit to visit and add the variable scopes. 167 | */ 168 | private void visitVariableScopes(SourceUnit sourceUnit) { 169 | VariableScopeVisitor scopeVisitor = new VariableScopeVisitor(sourceUnit); 170 | sourceUnit.AST.classes.each { 171 | scopeVisitor.visitClass(it) 172 | } 173 | } 174 | 175 | /** 176 | * Determine if the class trying to use MemoizeObject annotation has the needed imports. 177 | * @param sourceUnit SourceUnit to detect and/or inject import into 178 | * @param importClass Class of the import 179 | */ 180 | private void injectImport(SourceUnit sourceUnit, Class importClass) { 181 | if(!sourceUnit.AST.imports.any {it.className == ClassHelper.make(importClass).name} 182 | && !sourceUnit.AST.starImports.any {it.packageName == "${ClassHelper.make(importClass).packageName}."}) { 183 | sourceUnit.AST.addImport(importClass.simpleName, ClassHelper.make(importClass)) 184 | } 185 | } 186 | 187 | } -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/ast/MemoizeScoreASTTransformation.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis.ast 2 | 3 | import org.codehaus.groovy.ast.ASTNode 4 | import org.codehaus.groovy.ast.expr.ConstantExpression 5 | import org.codehaus.groovy.control.CompilePhase 6 | import org.codehaus.groovy.control.SourceUnit 7 | import org.codehaus.groovy.transform.GroovyASTTransformation 8 | 9 | @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) 10 | class MemoizeScoreASTTransformation extends MemoizeASTTransformation { 11 | 12 | @Override 13 | protected void generateMemoizeProperties(ASTNode[] astNodes, SourceUnit sourceUnit, Map memoizeProperties) { 14 | super.generateMemoizeProperties(astNodes, sourceUnit, memoizeProperties) 15 | def member = astNodes[0]?.members?.member?.value 16 | 17 | if(!member || member?.class != String) { 18 | addError('Internal Error: member is required for score', astNodes[0], sourceUnit) 19 | return 20 | } 21 | 22 | memoizeProperties.put(MEMBER, member) 23 | } 24 | 25 | @Override 26 | protected ConstantExpression makeRedisServiceConstantExpression() { 27 | new ConstantExpression('memoizeScore') 28 | } 29 | } -------------------------------------------------------------------------------- /plugin/src/main/groovy/grails/plugins/redis/util/RedisConfigurationUtil.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis.util 2 | 3 | import groovy.util.logging.Slf4j 4 | import redis.clients.jedis.JedisPool 5 | import redis.clients.jedis.JedisPoolConfig 6 | import redis.clients.jedis.JedisSentinelPool 7 | import redis.clients.jedis.Protocol 8 | 9 | /** 10 | * This class provides a closure that can (and must) be used within the context of a BeanBuilder. 11 | * To wire all redisServices using a custom class do the following 12 | * 13 | * def configureService = RedisConfigurationUtil.configureService 14 | * def redisConfigMap = application.config.grails.redis ?: [:] 15 | * 16 | * configureService.delegate = delegate 17 | * configureService(redisConfigMap, "", MyRedisService) 18 | * redisConfigMap?.connections?.each { connection -> 19 | * configureService(connection.value, connection?.key?.capitalize(), MyRedisService) 20 | *}* 21 | */ 22 | @Slf4j 23 | class RedisConfigurationUtil { 24 | 25 | /** 26 | * delegate to wire up the required beans. 27 | */ 28 | static def configureService = { delegate, redisConfigMap, key, serviceClass -> 29 | 30 | def poolBean = "redisPoolConfig${key}" 31 | def validPoolProperties = findValidPoolProperties(redisConfigMap.poolConfig) 32 | 33 | //todo: fix the validPoolProperty eval or just add them inline 34 | delegate."${poolBean}"(JedisPoolConfig) { 35 | validPoolProperties.each { configKey, value -> 36 | delegate.setProperty(configKey, value) 37 | } 38 | } 39 | // delegate."${poolBean}"(JedisPoolConfig) { bean -> 40 | // validPoolProperties.each { configKey, value -> 41 | // bean.setProperty(configKey, value) 42 | //// bean[configKey] = value 43 | // if(bean.class.) 44 | // bean."${configKey}" = value 45 | // } 46 | // } 47 | 48 | delegate.with { 49 | def host = redisConfigMap?.host ?: 'localhost' 50 | def port = redisConfigMap.containsKey("port") ? "${redisConfigMap.port}" as Integer : Protocol.DEFAULT_PORT 51 | def timeout = redisConfigMap.containsKey("timeout") ? "${redisConfigMap?.timeout}" as Integer : Protocol.DEFAULT_TIMEOUT 52 | def password = redisConfigMap?.password ?: null 53 | def database = redisConfigMap?.database ?: Protocol.DEFAULT_DATABASE 54 | def sentinels = redisConfigMap?.sentinels ?: null 55 | def masterName = redisConfigMap?.masterName ?: null 56 | def useSSL = redisConfigMap?.useSSL ?: false 57 | 58 | // If sentinels and a masterName is present, using different pool implementation 59 | if (sentinels && masterName) { 60 | if (sentinels instanceof String) { 61 | sentinels = Eval.me(sentinels.toString()) 62 | } 63 | 64 | if (sentinels instanceof Collection) { 65 | "redisPool${key}"(JedisSentinelPool, masterName, sentinels as Set, ref(poolBean), timeout, password, database, useSSL) { bean -> 66 | bean.destroyMethod = 'destroy' 67 | } 68 | } else { 69 | throw new RuntimeException('Redis configuraiton property [sentinels] does not appear to be a valid collection.') 70 | } 71 | } else { 72 | "redisPool${key}"(JedisPool, ref(poolBean), host, port, timeout, password, database, useSSL) { bean -> 73 | bean.destroyMethod = 'destroy' 74 | } 75 | } 76 | 77 | "redisService${key}"(serviceClass) { 78 | redisPool = ref("redisPool${key}") 79 | } 80 | } 81 | } 82 | 83 | static def findValidPoolProperties(def properties) { 84 | def fakeJedisPoolConfig = new JedisPoolConfig() 85 | properties?.findAll { configKey, value -> 86 | try { 87 | fakeJedisPoolConfig[configKey] = value 88 | return true 89 | } catch (Exception ignore) { 90 | log.warn('Redis pool configuration parameter ({}) does not exist on JedisPoolConfig or value is the wrong type', configKey.toString()) 91 | return false 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /plugin/src/main/groovy/redis/RedisGrailsPlugin.groovy: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import grails.plugins.Plugin 4 | import grails.plugins.redis.RedisService 5 | import grails.plugins.redis.util.RedisConfigurationUtil 6 | 7 | class RedisGrailsPlugin extends Plugin { 8 | 9 | def grailsVersion = "7.0.0-SNAPSHOT > *" 10 | def pluginExcludes = [ 11 | "codenarc.properties", 12 | "grails-app/conf/**", 13 | "grails-app/views/**", 14 | "grails-app/domain/**", 15 | "grails-app/services/test/**" 16 | ] 17 | 18 | def title = "Redis Plugin" // Headline display name of the plugin 19 | def author = "Ted Naleid" 20 | def authorEmail = "contact@naleid.com" 21 | 22 | def description = '''The Redis plugin provides integration with a Redis datastore. Redis is a lightning fast 'data structure server'. The plugin enables a number of memoization techniques to cache results from complex operations in Redis.''' 23 | def issueManagement = [system: 'github', url: 'https://github.com/grails-plugins/grails-redis/issues'] 24 | def scm = [url: "https://github.com/grails-plugins/grails-redis"] 25 | def documentation = "http://grails.org/plugin/grails-redis" 26 | def license = "APACHE" 27 | 28 | def developers = [ 29 | [name: "Burt Beckwith"], 30 | [name: "Christian Oestreich"], 31 | [name: "Brian Coles"], 32 | [name: "Michael Cameron"], 33 | [name: "John Engelman"], 34 | [name: "David Seiler"], 35 | [name: "Jordon Saardchit"], 36 | [name: "Florian Langenhahn"], 37 | [name: "German Sancho"], 38 | [name: "John Mulhern"], 39 | [name: "Shaun Jurgemeyer"]] 40 | 41 | Closure doWithSpring() { 42 | { -> 43 | def redisConfigMap = grailsApplication.config.getProperty('grails.redis') ?: [:] 44 | 45 | RedisConfigurationUtil.configureService(delegate, redisConfigMap, "", RedisService) 46 | redisConfigMap?.connections?.each { connection -> 47 | RedisConfigurationUtil.configureService(delegate, connection.value, connection?.key?.capitalize(), RedisService) 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /plugin/src/test/groovy/grails/plugins/redis/RedisServiceSpec.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis 2 | 3 | import grails.core.GrailsApplication 4 | import grails.spring.BeanBuilder 5 | import grails.testing.mixin.integration.Integration 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import redis.clients.jedis.Jedis 8 | import redis.clients.jedis.Transaction 9 | import redis.clients.jedis.exceptions.JedisConnectionException 10 | import spock.lang.Specification 11 | 12 | import java.util.concurrent.* 13 | 14 | import static grails.plugins.redis.RedisService.NO_EXPIRATION_TTL 15 | 16 | @Integration 17 | class RedisServiceSpec extends Specification { 18 | @Autowired RedisService redisService 19 | @Autowired GrailsApplication grailsApplication 20 | RedisService redisServiceMock 21 | 22 | def "application context wired up"() { 23 | when: 24 | def app = grailsApplication 25 | 26 | then: 27 | app 28 | } 29 | 30 | void setup() { 31 | redisServiceMock = mockRedisServiceForFailureTest(getNewInstanceOfBean(RedisService)) 32 | try { 33 | redisService.flushDB() 34 | } 35 | catch (JedisConnectionException jce) { 36 | // swallow connect exception so failure tests can proceed 37 | } 38 | } 39 | 40 | def "attempt pool exhaustion and redis.close()"() { 41 | given: 42 | Integer loopCount = 50 43 | ExecutorService taskExecutor = Executors.newWorkStealingPool(loopCount) 44 | CountDownLatch latch = new CountDownLatch(loopCount) 45 | ConcurrentMap exceptionStatusMap = new ConcurrentHashMap<>() 46 | 47 | when: 48 | List tasks = [] 49 | loopCount.times { Integer loop -> 50 | tasks.add(new Callable(){ 51 | @Override 52 | String call() throws Exception { 53 | Boolean hasException = false 54 | try { 55 | redisService.withRedis { Jedis redis -> 56 | println "Starting ${loop}" 57 | Thread.sleep(2000) 58 | latch.countDown() 59 | println "Completed ${loop}" 60 | } 61 | } catch (Exception e) { 62 | hasException = true 63 | } finally { 64 | exceptionStatusMap.putIfAbsent(loop, hasException) 65 | } 66 | } 67 | }) 68 | } 69 | taskExecutor.invokeAll(tasks) 70 | 71 | try { 72 | latch.await() 73 | taskExecutor.shutdown() 74 | } catch (Exception e) { 75 | assert false 76 | } 77 | 78 | then: 79 | !exceptionStatusMap.containsValue(true) 80 | } 81 | 82 | def "attempt pool exhaustion and redis.close() through exeptions"() { 83 | given: 84 | Integer loopCount = 50 85 | ExecutorService taskExecutor = Executors.newWorkStealingPool(loopCount) 86 | CountDownLatch latch = new CountDownLatch(loopCount) 87 | ConcurrentMap exceptionStatusMap = new ConcurrentHashMap<>() 88 | 89 | when: 90 | List tasks = [] 91 | loopCount.times { Integer loop -> 92 | tasks.add(new Callable(){ 93 | @Override 94 | String call() throws Exception { 95 | Boolean hasException = false 96 | try { 97 | redisService.withRedis { Jedis redis -> 98 | latch.countDown() 99 | throw new RuntimeException("BOOM!") 100 | } 101 | } catch (Exception e) { 102 | hasException = true 103 | } finally { 104 | exceptionStatusMap.putIfAbsent(loop, hasException) 105 | } 106 | } 107 | }) 108 | } 109 | taskExecutor.invokeAll(tasks) 110 | 111 | try { 112 | latch.await() 113 | taskExecutor.shutdown() 114 | } catch (Exception e) { 115 | assert false 116 | } 117 | 118 | then: 119 | exceptionStatusMap.containsValue(true) 120 | } 121 | 122 | def testFlushDB() { 123 | given: 124 | // actually called as part of setup too, but we can test it here 125 | redisService.withRedis { Jedis redis -> 126 | assert 0 == redis.dbSize() 127 | redis.set("foo", "bar") 128 | assert 1 == redis.dbSize() 129 | } 130 | 131 | when: 132 | def size = -1 133 | redisService.flushDB() 134 | redisService.withRedis { Jedis redis -> 135 | size = redis.dbSize() 136 | } 137 | 138 | then: 139 | size == 0 140 | } 141 | 142 | def testMemoizeKeyWithNoRedis() { 143 | given: 144 | def calledCount = 0 145 | 146 | def cacheMissClosure = { 147 | calledCount += 1 148 | return "foo" 149 | } 150 | 151 | when: 152 | def cacheMissResult = redisServiceMock.memoize("mykey", cacheMissClosure) 153 | 154 | then: 155 | 1 == calledCount 156 | "foo" == cacheMissResult 157 | 158 | when: 159 | cacheMissResult = redisServiceMock.memoize("mykey", cacheMissClosure) 160 | 161 | then: 162 | 2 == calledCount 163 | "foo" == cacheMissResult 164 | } 165 | 166 | 167 | void testMemoizeKey() { 168 | given: 169 | def calledCount = 0 170 | def cacheMissClosure = { 171 | calledCount += 1 172 | return "foo" 173 | } 174 | 175 | when: 176 | def cacheMissResult = redisService.memoize("mykey", cacheMissClosure) 177 | 178 | then: 179 | 1 == calledCount 180 | "foo" == cacheMissResult 181 | NO_EXPIRATION_TTL == redisService.ttl("mykey") 182 | 183 | when: 184 | def cacheHitResult = redisService.memoize("mykey", cacheMissClosure) 185 | 186 | then: "should have hit the cache, not called our method again" 187 | 1 == calledCount 188 | "foo" == cacheHitResult 189 | } 190 | 191 | 192 | def testMemoizeKeyWithExpire() { 193 | given: 194 | assert 0 > redisService.ttl("mykey") 195 | 196 | when: 197 | def result = redisService.memoize("mykey", 60) { "foo" } 198 | 199 | then: 200 | "foo" == result 201 | NO_EXPIRATION_TTL < redisService.ttl("mykey") 202 | } 203 | 204 | void testMemoizeKeyNullValue() { 205 | given: 206 | def calledCount = 0 207 | def cacheMissClosure = { 208 | calledCount += 1 209 | return null 210 | } 211 | 212 | when: 213 | def cacheMissResult = redisServiceMock.memoize("mykey", cacheMissClosure) 214 | 215 | then: 216 | assert 1 == calledCount 217 | assert null == cacheMissResult 218 | 219 | when: 220 | def cacheMissAgainResult = redisServiceMock.memoize("mykey", cacheMissClosure) 221 | 222 | then: "should have called the method again if we got a null" 223 | assert 2 == calledCount 224 | assert null == cacheMissAgainResult 225 | } 226 | 227 | 228 | void testMemoizeHashFieldWithoutRedis() { 229 | given: 230 | def calledCount = 0 231 | def cacheMissClosure = { 232 | calledCount += 1 233 | return "foo" 234 | } 235 | 236 | when: 237 | def cacheMissResult = redisServiceMock.memoizeHashField("mykey", "first", cacheMissClosure) 238 | 239 | then: 240 | 1 == calledCount 241 | "foo" == cacheMissResult 242 | 243 | when: 244 | cacheMissResult = redisServiceMock.memoizeHashField("mykey", "first", cacheMissClosure) 245 | 246 | then: "should have hit the cache, not called our method again" 247 | 2 == calledCount 248 | "foo" == cacheMissResult 249 | } 250 | 251 | 252 | void testMemoizeHashField() { 253 | given: 254 | def calledCount = 0 255 | def cacheMissClosure = { 256 | calledCount += 1 257 | return "foo" 258 | } 259 | 260 | when: 261 | def cacheMissResult = redisService.memoizeHashField("mykey", "first", cacheMissClosure) 262 | 263 | then: 264 | 1 == calledCount 265 | "foo" == cacheMissResult 266 | NO_EXPIRATION_TTL == redisService.ttl("mykey") 267 | 268 | when: 269 | def cacheHitResult = redisService.memoizeHashField("mykey", "first", cacheMissClosure) 270 | 271 | then: "should have hit the cache, not called our method again" 272 | 1 == calledCount 273 | "foo" == cacheHitResult 274 | 275 | when: 276 | def cacheMissSecondResult = redisService.memoizeHashField("mykey", "second", cacheMissClosure) 277 | 278 | then: "cache miss because we're using a different field in the same key" 279 | 2 == calledCount 280 | "foo" == cacheMissSecondResult 281 | } 282 | 283 | 284 | void testMemoizeHashFieldWithExpire() { 285 | given: 286 | assert 0 > redisService.ttl("mykey") 287 | 288 | when: 289 | def result = redisService.memoizeHashField("mykey", "first", 60) { "foo" } 290 | 291 | then: 292 | "foo" == result 293 | NO_EXPIRATION_TTL < redisService.ttl("mykey") 294 | } 295 | 296 | void testMemoizeHashWithoutRedis() { 297 | given: 298 | def calledCount = 0 299 | def expectedHash = [foo: 'bar', baz: 'qux'] 300 | def cacheMissClosure = { 301 | calledCount += 1 302 | return expectedHash 303 | } 304 | 305 | when: 306 | def cacheMissResult = redisServiceMock.memoizeHash("mykey", cacheMissClosure) 307 | 308 | then: 309 | 1 == calledCount 310 | expectedHash == cacheMissResult 311 | 312 | when: 313 | def cacheHitResult = redisServiceMock.memoizeHash("mykey", cacheMissClosure) 314 | 315 | then: "should have hit the cache, not called our method again" 316 | 2 == calledCount 317 | expectedHash == cacheHitResult 318 | } 319 | 320 | 321 | void testMemoizeHash() { 322 | given: 323 | def calledCount = 0 324 | def expectedHash = [foo: 'bar', baz: 'qux'] 325 | def cacheMissClosure = { 326 | calledCount += 1 327 | return expectedHash 328 | } 329 | 330 | when: 331 | def cacheMissResult = redisService.memoizeHash("mykey", cacheMissClosure) 332 | 333 | then: 334 | assert 1 == calledCount 335 | assert expectedHash == cacheMissResult 336 | assert NO_EXPIRATION_TTL == redisService.ttl("mykey") 337 | 338 | when: 339 | def cacheHitResult = redisService.memoizeHash("mykey", cacheMissClosure) 340 | 341 | then: "should have hit the cache, not called our method again" 342 | assert 1 == calledCount 343 | assert expectedHash == cacheHitResult 344 | } 345 | 346 | 347 | void testMemoizeHashWithExpire() { 348 | given: 349 | def expectedHash = [foo: 'bar', baz: 'qux'] 350 | assert 0 > redisService.ttl("mykey") 351 | 352 | when: 353 | def result = redisService.memoizeHash("mykey", 60) { expectedHash } 354 | 355 | then: 356 | assert expectedHash == result 357 | assert NO_EXPIRATION_TTL < redisService.ttl("mykey") 358 | } 359 | 360 | void testMemoizeListWithoutRedis() { 361 | given: 362 | def book1 = "book1" 363 | def book2 = "book2" 364 | def book3 = "book3" 365 | List books = [book1, book2, book3] 366 | 367 | def calledCount = 0 368 | def cacheMissClosure = { 369 | calledCount += 1 370 | return books 371 | } 372 | 373 | when: 374 | def cacheMissList = redisServiceMock.memoizeList("mykey", cacheMissClosure) 375 | 376 | then: 377 | assert 1 == calledCount 378 | assert [book1, book2, book3] == cacheMissList 379 | 380 | when: 381 | List cacheHitList = redisServiceMock.memoizeList("mykey", cacheMissClosure) 382 | 383 | then: "cache hit, don't call closure again" 384 | assert 2 == calledCount 385 | assert [book1, book2, book3] == cacheHitList 386 | assert cacheMissList == cacheHitList 387 | } 388 | 389 | 390 | void testMemoizeList() { 391 | given: 392 | def book1 = "book1" 393 | def book2 = "book2" 394 | def book3 = "book3" 395 | List books = [book1, book2, book3] 396 | 397 | def calledCount = 0 398 | def cacheMissClosure = { 399 | calledCount += 1 400 | return books 401 | } 402 | 403 | when: 404 | def cacheMissList = redisService.memoizeList("mykey", cacheMissClosure) 405 | 406 | then: 407 | 1 == calledCount 408 | [book1, book2, book3] == cacheMissList 409 | NO_EXPIRATION_TTL == redisService.ttl("mykey") 410 | 411 | when: 412 | List cacheHitList = redisService.memoizeList("mykey", cacheMissClosure) 413 | 414 | then: "cache hit, don't call closure again" 415 | assert 1 == calledCount 416 | assert [book1, book2, book3] == cacheHitList 417 | assert cacheMissList == cacheHitList 418 | } 419 | 420 | 421 | void testMemoizeListWithExpire() { 422 | given: 423 | def book1 = "book1" 424 | assert 0 > redisService.ttl("mykey") 425 | 426 | when: 427 | def result = redisService.memoizeList("mykey", 60) { [book1] } 428 | 429 | then: 430 | [book1] == result 431 | NO_EXPIRATION_TTL < redisService.ttl("mykey") 432 | } 433 | 434 | 435 | void testMemoizeSetWithoutRedis() { 436 | given: 437 | def book1 = "book1" 438 | def book2 = "book2" 439 | def book3 = "book3" 440 | def bookSet = [book1, book2, book3] as Set 441 | 442 | def calledCount = 0 443 | def cacheMissClosure = { 444 | calledCount += 1 445 | return bookSet 446 | } 447 | 448 | when: 449 | Set cacheMissSet = redisServiceMock.memoizeSet("mykey", cacheMissClosure) 450 | 451 | then: 452 | 1 == calledCount 453 | [book1, book2, book3] as Set == cacheMissSet 454 | 455 | when: 456 | def cacheHitSet = redisServiceMock.memoizeSet("mykey", cacheMissClosure) 457 | 458 | then: "cache hit, don't call closure again" 459 | 2 == calledCount 460 | [book1, book2, book3] as Set == cacheHitSet 461 | cacheMissSet == cacheHitSet 462 | } 463 | 464 | 465 | void testMemoizeSet() { 466 | given: 467 | def book1 = "book1" 468 | def book2 = "book2" 469 | def book3 = "book3" 470 | def bookSet = [book1, book2, book3] as Set 471 | 472 | def calledCount = 0 473 | def cacheMissClosure = { 474 | calledCount += 1 475 | return bookSet 476 | } 477 | 478 | when: 479 | Set cacheMissSet = redisService.memoizeSet("mykey", cacheMissClosure) 480 | 481 | then: 482 | 1 == calledCount 483 | [book1, book2, book3] as Set == cacheMissSet 484 | NO_EXPIRATION_TTL == redisService.ttl("mykey") 485 | 486 | when: 487 | def cacheHitSet = redisService.memoizeSet("mykey", cacheMissClosure) 488 | 489 | then: "cache hit, don't call closure again" 490 | 1 == calledCount 491 | [book1, book2, book3] as Set == cacheHitSet 492 | cacheMissSet == cacheHitSet 493 | } 494 | 495 | 496 | void testMemoizeSetWithExpire() { 497 | given: 498 | def book1 = "book1" 499 | assert 0 > redisService.ttl("mykey") 500 | 501 | when: 502 | def result = redisService.memoizeSet("mykey", 60) { [book1] as Set } 503 | 504 | then: 505 | [book1] as Set == result 506 | NO_EXPIRATION_TTL < redisService.ttl("mykey") 507 | } 508 | 509 | 510 | void testMemoizeObject_simpleMapOfStrings() { 511 | given: 512 | Map map = [foo: "bar", baz: "qux"] 513 | 514 | def calledCount = 0 515 | def cacheMissClosure = { 516 | calledCount += 1 517 | map 518 | } 519 | when: 520 | def cacheMissValue = redisService.memoizeObject(Map.class, "mykey", cacheMissClosure) 521 | then: 522 | 1 == calledCount 523 | "bar" == cacheMissValue.foo 524 | "qux" == cacheMissValue.baz 525 | NO_EXPIRATION_TTL == redisService.ttl("mykey") 526 | 527 | when: 528 | def cacheHitValue = redisService.memoizeObject(Map.class, "mykey", cacheMissClosure) 529 | 530 | then: 531 | 1 == calledCount 532 | "bar" == cacheHitValue.foo 533 | "qux" == cacheHitValue.baz 534 | } 535 | 536 | 537 | void testMemoizeObject_withTTL() { 538 | given: 539 | Map map = [foo: "bar", baz: "qux"] 540 | assert 0 > redisService.ttl("mykey") 541 | 542 | when: 543 | def cacheMissValue = redisService.memoizeObject(Map.class, "mykey", 60) { -> map } 544 | 545 | then: 546 | "bar" == cacheMissValue.foo 547 | "qux" == cacheMissValue.baz 548 | NO_EXPIRATION_TTL < redisService.ttl("mykey") 549 | } 550 | 551 | 552 | void testMemoizeObject_nullValue() { 553 | given: 554 | Map map = null 555 | 556 | def calledCount = 0 557 | def cacheMissClosure = { 558 | calledCount += 1 559 | map 560 | } 561 | when: 562 | def cacheMissValue = redisService.memoizeObject(Map.class, "mykey", cacheMissClosure) 563 | then: 564 | 1 == calledCount 565 | null == cacheMissValue 566 | when: 567 | def cacheHitValue = redisService.memoizeObject(Map.class, "mykey", cacheMissClosure) 568 | then: 569 | 1 == calledCount 570 | null == cacheHitValue 571 | } 572 | 573 | 574 | void testMemoizeObject_nullValue_cacheNullFalse() { 575 | given: 576 | Map map = null 577 | 578 | def calledCount = 0 579 | def cacheMissClosure = { 580 | calledCount += 1 581 | map 582 | } 583 | 584 | when: 585 | def cacheMissValue = redisService.memoizeObject(Map.class, "mykey", [cacheNull: false], cacheMissClosure) 586 | 587 | then: 588 | 1 == calledCount 589 | null == cacheMissValue 590 | 591 | when: 592 | def cacheMissAgainValue = redisService.memoizeObject(Map.class, "mykey", [cacheNull: false], cacheMissClosure) 593 | 594 | then: 595 | 2 == calledCount 596 | null == cacheMissAgainValue 597 | } 598 | 599 | 600 | void testDeleteKeysWithPattern() { 601 | given: 602 | def calledCount = 0 603 | def cacheMissClosure = { 604 | calledCount += 1 605 | return "foobar" 606 | } 607 | 608 | when: 609 | redisService.memoize("mykey:1", cacheMissClosure) 610 | redisService.memoize("mykey:2", cacheMissClosure) 611 | 612 | then: 613 | 2 == calledCount 614 | 615 | when: 616 | redisService.memoize("mykey:1", cacheMissClosure) 617 | redisService.memoize("mykey:2", cacheMissClosure) 618 | 619 | then: "call count shouldn't increase" 620 | 2 == calledCount 621 | 622 | when: 623 | redisService.deleteKeysWithPattern("mykey:*") 624 | 625 | redisService.memoize("mykey:1", cacheMissClosure) 626 | redisService.memoize("mykey:2", cacheMissClosure) 627 | 628 | then: "Because we deleted those keys before and there is a cache miss" 629 | 4 == calledCount 630 | } 631 | 632 | 633 | void testWithTransaction() { 634 | given: 635 | def bar = '' 636 | 637 | when: 638 | redisService.withRedis { Jedis redis -> 639 | assert redis.get("foo") == null 640 | redisService.withTransaction { Transaction transaction -> 641 | transaction.set("foo", "bar") 642 | assert redis.get("foo") == null 643 | } 644 | bar = redis.get("foo") 645 | } 646 | 647 | then: 648 | bar == "bar" 649 | } 650 | 651 | 652 | void testWithTransactionClosureException() { 653 | given: 654 | def foo = "foo" 655 | def fooNew = "foo" 656 | redisService.withRedis { Jedis redis -> 657 | foo = redis.get("foo") 658 | } 659 | 660 | when: 661 | try { 662 | redisService.withTransaction { Transaction transaction -> 663 | transaction.set("foo", "bar") 664 | throw new Exception("Something bad happened") 665 | } 666 | } catch (Exception e) { 667 | assert e.message =~ /bad/ 668 | } 669 | 670 | then: 671 | foo == null 672 | 673 | when: 674 | redisService.withRedis { Jedis redis -> 675 | fooNew = redis.get("foo") 676 | } 677 | 678 | then: 679 | fooNew == null 680 | } 681 | 682 | 683 | void testPropertyMissingGetterRetrievesStringValue() { 684 | given: 685 | assert redisService.foo == null 686 | 687 | when: 688 | redisService.withRedis { Jedis redis -> 689 | redis.set("foo", "bar") 690 | } 691 | 692 | then: 693 | "bar" == redisService.foo 694 | } 695 | 696 | 697 | void testPropertyMissingSetterSetsStringValue() { 698 | given: 699 | def bar = "" 700 | redisService.withRedis { Jedis redis -> 701 | assert redis.foo == null 702 | } 703 | 704 | when: 705 | redisService.foo = "bar" 706 | 707 | then: 708 | redisService.withRedis { Jedis redis -> 709 | bar = redis.foo 710 | } 711 | bar == "bar" 712 | } 713 | 714 | def testMethodMissingDelegatesToJedis() { 715 | given: 716 | assert redisService.foo == null 717 | 718 | when: 719 | redisService.set("foo", "bar") 720 | 721 | then: 722 | assert "bar" == redisService.foo 723 | } 724 | 725 | 726 | def testMethodNotOnJedisThrowsMethodMissingException() { 727 | when: 728 | def result = "" 729 | try { 730 | redisService.methodThatDoesNotExistAndNeverWill() 731 | } catch (Exception e) { 732 | result = e.message 733 | } 734 | 735 | then: 736 | result?.startsWith("No signature of method: redis.clients.jedis.Jedis.methodThatDoesNotExistAndNeverWill") 737 | } 738 | 739 | // utility method for assisting in test setup 740 | 741 | RedisService getNewInstanceOfBean(Class clazz) { 742 | String beanName = "prototype${clazz.name}" 743 | BeanBuilder beanBuilder = new BeanBuilder(grailsApplication.mainContext) 744 | 745 | beanBuilder.beans { 746 | "$beanName"(clazz) { bean -> 747 | bean.autowire = 'byName' 748 | } 749 | } 750 | 751 | beanBuilder.createApplicationContext().getBean(beanName) 752 | } 753 | 754 | def mockRedisServiceForFailureTest(RedisService svc) { 755 | def redisPoolMock = new Object() 756 | redisPoolMock.metaClass.getResource = { -> 757 | throw new JedisConnectionException('Generated by a mocked redisPool') 758 | } 759 | svc.redisPool = redisPoolMock 760 | return svc 761 | } 762 | 763 | } 764 | -------------------------------------------------------------------------------- /plugin/src/test/groovy/grails/plugins/redis/RedisTagLibSpec.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugins.redis 2 | 3 | import grails.core.GrailsApplication 4 | import grails.gorm.transactions.Rollback 5 | import grails.testing.mixin.integration.Integration 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import spock.lang.Ignore 8 | import spock.lang.Specification 9 | 10 | @Integration 11 | @Rollback 12 | class RedisTagLibSpec extends Specification { 13 | 14 | @Autowired GrailsApplication grailsApplication 15 | @Autowired RedisService redisService 16 | RedisTagLib tagLib 17 | 18 | protected static KEY = "RedisTagLibTests:memoize" 19 | protected static CONTENTS = "expected contents" 20 | protected static FAIL_BODY = "unexpected contents, should not have this" 21 | 22 | def setup() { 23 | redisService.flushDB() 24 | tagLib= grailsApplication.mainContext.getBean(RedisTagLib) 25 | } 26 | 27 | @Ignore 28 | def testMemoize() { 29 | when: 30 | String result = tagLib.memoize([key: KEY], { -> CONTENTS }) 31 | 32 | then: 33 | CONTENTS == result 34 | 35 | when: 36 | result = tagLib.memoize([key: KEY], { -> FAIL_BODY }) 37 | then: 38 | CONTENTS == result // won't find $FAIL_BODY 39 | } 40 | 41 | @Ignore 42 | def testMemoizeTTL() { 43 | when: 44 | String result = tagLib.memoize([key: 'no-ttl-test'], { -> CONTENTS }).toString() 45 | 46 | then: 47 | CONTENTS == result 48 | redisService.NO_EXPIRATION_TTL == redisService.ttl("no-ttl-test") 49 | 50 | when: 51 | result = tagLib.memoize([key: 'ttl-test', expire: 60], { -> CONTENTS }).toString() 52 | 53 | then: 54 | CONTENTS == result 55 | redisService.ttl("ttl-test") > 0 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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-redis') 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-redis-root' 28 | 29 | include 'plugin' 30 | findProject(':plugin').name = 'grails-redis' 31 | 32 | include 'examples-redis-demo' 33 | project(':examples-redis-demo').projectDir = new File(settingsDir, 'examples/redis-demo') 34 | --------------------------------------------------------------------------------