├── .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 | Grails Runtime ExceptionError
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | - An error has occurred
19 | - Exception: ${exception}
20 | - Message: ${message}
21 | - Path: ${path}
22 |
23 |
24 |
25 |
26 |
27 | - An error has occurred
28 |
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 |
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 |
--------------------------------------------------------------------------------