├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── dasburo │ │ └── spring │ │ └── cache │ │ └── dynamo │ │ ├── DefaultDynamoCacheWriter.java │ │ ├── DynamoCache.java │ │ ├── DynamoCacheBuilder.java │ │ ├── DynamoCacheConfiguration.java │ │ ├── DynamoCacheManager.java │ │ ├── DynamoCacheWriter.java │ │ ├── autoconfigure │ │ ├── DynamoCacheAutoConfiguration.java │ │ ├── DynamoCacheProperties.java │ │ └── DynamoCachePropertiesList.java │ │ ├── rootattribute │ │ ├── RootAttribute.java │ │ ├── RootAttributeConfig.java │ │ └── RootAttributeReader.java │ │ ├── serializer │ │ ├── DynamoSerializer.java │ │ ├── GZipSerializer.java │ │ ├── Jackson2JsonSerializer.java │ │ ├── OxmSerializer.java │ │ ├── SerializableSerializer.java │ │ ├── SerializationException.java │ │ ├── SerializationUtils.java │ │ └── StringSerializer.java │ │ └── util │ │ ├── ByteUtils.java │ │ └── TableUtils.java └── resources │ └── META-INF │ └── spring.factories └── test └── java └── com └── dasburo └── spring └── cache └── dynamo ├── DynamoCacheManagerTest.java ├── DynamoCacheTest.java ├── TestConfiguration.java ├── TestDbCreationExtension.java ├── UnitTestBase.java ├── autoconfigure └── DynamoCacheAutoConfigurationTest.java ├── helper ├── Address.java ├── Company.java └── NotSerializeable.java ├── rootattribute ├── RootAttributeConfigTest.java ├── RootAttributeReaderTest.java └── SampleTestClass.java ├── serializer ├── GZipSerializerTest.java ├── Jackson2JsonSerializerTest.java ├── OxmSerializerTest.java ├── SerializableSerializerTest.java ├── SerializationUtilsTest.java └── StringSerializerTest.java └── util └── TableUtilsTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,java,linux,maven,windows,intellij,jetbrains+all 3 | # Edit at https://www.gitignore.io/?templates=osx,java,linux,maven,windows,intellij,jetbrains+all 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | native-libs/** 19 | 20 | # Sensitive or high-churn files 21 | .idea/**/dataSources/ 22 | .idea/**/dataSources.ids 23 | .idea/**/dataSources.local.xml 24 | .idea/**/sqlDataSources.xml 25 | .idea/**/dynamic.xml 26 | .idea/**/uiDesigner.xml 27 | .idea/**/dbnavigator.xml 28 | 29 | # Gradle 30 | .idea/**/gradle.xml 31 | .idea/**/libraries 32 | 33 | # Gradle and Maven with auto-import 34 | # When using Gradle or Maven with auto-import, you should exclude module files, 35 | # since they will be recreated, and may cause churn. Uncomment if using 36 | # auto-import. 37 | # .idea/modules.xml 38 | # .idea/*.iml 39 | # .idea/modules 40 | # *.iml 41 | # *.ipr 42 | 43 | # CMake 44 | cmake-build-*/ 45 | 46 | # File-based project format 47 | *.iws 48 | 49 | # IntelliJ 50 | out/ 51 | 52 | # mpeltonen/sbt-idea plugin 53 | .idea_modules/ 54 | 55 | # JIRA plugin 56 | atlassian-ide-plugin.xml 57 | 58 | # Cursive Clojure plugin 59 | .idea/replstate.xml 60 | 61 | # Crashlytics plugin (for Android Studio and IntelliJ) 62 | com_crashlytics_export_strings.xml 63 | crashlytics.properties 64 | crashlytics-build.properties 65 | fabric.properties 66 | 67 | # Editor-based Rest Client 68 | .idea/httpRequests 69 | 70 | # Android studio 3.1+ serialized cache file 71 | .idea/caches/build_file_checksums.ser 72 | 73 | ### Intellij Patch ### 74 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 75 | 76 | # *.iml 77 | # modules.xml 78 | # .idea/misc.xml 79 | # *.ipr 80 | 81 | # Sonarlint plugin 82 | .idea/sonarlint 83 | 84 | ### Java ### 85 | # Compiled class file 86 | *.class 87 | 88 | # Log file 89 | *.log 90 | 91 | # BlueJ files 92 | *.ctxt 93 | 94 | # Mobile Tools for Java (J2ME) 95 | .mtj.tmp/ 96 | 97 | # Package Files # 98 | *.jar 99 | *.war 100 | *.nar 101 | *.ear 102 | *.zip 103 | *.tar.gz 104 | *.rar 105 | 106 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 107 | hs_err_pid* 108 | 109 | ### JetBrains+all ### 110 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 111 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 112 | 113 | # User-specific stuff 114 | 115 | # Generated files 116 | 117 | # Sensitive or high-churn files 118 | 119 | # Gradle 120 | 121 | # Gradle and Maven with auto-import 122 | # When using Gradle or Maven with auto-import, you should exclude module files, 123 | # since they will be recreated, and may cause churn. Uncomment if using 124 | # auto-import. 125 | # .idea/modules.xml 126 | # .idea/*.iml 127 | # .idea/modules 128 | # *.iml 129 | # *.ipr 130 | 131 | # CMake 132 | 133 | # File-based project format 134 | 135 | # IntelliJ 136 | 137 | # mpeltonen/sbt-idea plugin 138 | 139 | # JIRA plugin 140 | 141 | # Cursive Clojure plugin 142 | 143 | # Crashlytics plugin (for Android Studio and IntelliJ) 144 | 145 | # Editor-based Rest Client 146 | 147 | # Android studio 3.1+ serialized cache file 148 | 149 | ### JetBrains+all Patch ### 150 | # Ignores the whole .idea folder and all .iml files 151 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 152 | 153 | .idea/ 154 | 155 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 156 | 157 | *.iml 158 | modules.xml 159 | .idea/misc.xml 160 | *.ipr 161 | 162 | # Sonarlint plugin 163 | 164 | ### Linux ### 165 | *~ 166 | 167 | # temporary files which can be created if a process still has a handle open of a deleted file 168 | .fuse_hidden* 169 | 170 | # KDE directory preferences 171 | .directory 172 | 173 | # Linux trash folder which might appear on any partition or disk 174 | .Trash-* 175 | 176 | # .nfs files are created when an open file is removed but is still being accessed 177 | .nfs* 178 | 179 | ### Maven ### 180 | target/ 181 | pom.xml.tag 182 | pom.xml.releaseBackup 183 | pom.xml.versionsBackup 184 | pom.xml.next 185 | release.properties 186 | dependency-reduced-pom.xml 187 | buildNumber.properties 188 | .mvn/timing.properties 189 | .mvn/wrapper/maven-wrapper.jar 190 | 191 | ### OSX ### 192 | # General 193 | .DS_Store 194 | .AppleDouble 195 | .LSOverride 196 | 197 | # Icon must end with two \r 198 | Icon 199 | 200 | # Thumbnails 201 | ._* 202 | 203 | # Files that might appear in the root of a volume 204 | .DocumentRevisions-V100 205 | .fseventsd 206 | .Spotlight-V100 207 | .TemporaryItems 208 | .Trashes 209 | .VolumeIcon.icns 210 | .com.apple.timemachine.donotpresent 211 | 212 | # Directories potentially created on remote AFP share 213 | .AppleDB 214 | .AppleDesktop 215 | Network Trash Folder 216 | Temporary Items 217 | .apdisk 218 | 219 | ### Windows ### 220 | # Windows thumbnail cache files 221 | Thumbs.db 222 | Thumbs.db:encryptable 223 | ehthumbs.db 224 | ehthumbs_vista.db 225 | 226 | # Dump file 227 | *.stackdump 228 | 229 | # Folder config file 230 | [Dd]esktop.ini 231 | 232 | # Recycle Bin used on file shares 233 | $RECYCLE.BIN/ 234 | 235 | # Windows Installer files 236 | *.cab 237 | *.msi 238 | *.msix 239 | *.msm 240 | *.msp 241 | 242 | # Windows shortcuts 243 | *.lnk 244 | 245 | # End of https://www.gitignore.io/api/osx,java,linux,maven,windows,intellij,jetbrains+all -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 4 | script: 5 | - mvn test-compile && mvn test jacoco:report 6 | after_success: 7 | - mvn coveralls:report -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | # v2 8 | 9 | ## [Unreleased] 10 | ### Changed 11 | - check TTL for stored items, as items that have expired may still appear 12 | 13 | ## [2.0.0] - 2022-08-17 14 | ### Changed 15 | - use AWS Java SDK v2.x [@derXear](https://github.com/derXear) 16 | - update Spring Boot dependencies [@derXear](https://github.com/derXear) 17 | 18 | # v1 19 | 20 | ## [0.9.6] - 2020-11-13 21 | ### Changed 22 | - add getter for `DynamoCacheWriter` and `DynamoSerializer` in `DynamoCache` [@derXear](https://github.com/derXear) 23 | - update Amazon DynamoDB dependencies [@derXear](https://github.com/derXear) 24 | 25 | ## [0.9.5] - 2020-03-12 26 | ### Changed 27 | - the `DynamoDB::CreateTable` permission is not required anymore if the table already exist, 28 | instead at least `DynamoDB::DescribeTable` is necessary [@derXear](https://github.com/derXear) 29 | 30 | ## [0.9.4] - 2020-01-14 31 | ### Fixed 32 | - fix unit used for ttl from EpochMillis to EpochSeconds [@dnltsk](https://github.com/dnltsk) 33 | 34 | ## [0.9.3] - 2019-12-18 35 | ### Fixed 36 | - fix datatype used for ttl [@dnltsk](https://github.com/dnltsk) 37 | 38 | ## [0.9.2] - 2019-12-04 39 | ### Changed 40 | - fix javadoc and maven-gpg-plugin configuration by [@derXear](https://github.com/derXear) 41 | 42 | ## [0.9.1] - 2019-09-09 43 | ### Fixed 44 | - fix locking DefaultDynamoCacheWriter by [@derXear](https://github.com/derXear) 45 | 46 | ## [0.9.0] - 2019-09-06 47 | ### Added 48 | - initial code base by [@derXear](https://github.com/derXear) 49 | 50 | [Unreleased]: https://github.com/bad-opensource/spring-cache-dynamodb/compare/v2.0.0...HEAD 51 | [2.0.0]: https://github.com/bad-opensource/spring-cache-dynamodb/releases/tag/v2.0.0 52 | [0.9.6]: https://github.com/bad-opensource/spring-cache-dynamodb/releases/tag/v0.9.6 53 | [0.9.5]: https://github.com/bad-opensource/spring-cache-dynamodb/releases/tag/v0.9.5 54 | [0.9.4]: https://github.com/bad-opensource/spring-cache-dynamodb/releases/tag/v0.9.4 55 | [0.9.3]: https://github.com/bad-opensource/spring-cache-dynamodb/releases/tag/v0.9.3 56 | [0.9.2]: https://github.com/bad-opensource/spring-cache-dynamodb/releases/tag/v0.9.2 57 | [0.9.1]: https://github.com/bad-opensource/spring-cache-dynamodb/releases/tag/v0.9.1 58 | [0.9.0]: https://github.com/bad-opensource/spring-cache-dynamodb/releases/tag/v0.9.0 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Das Büro am Draht GmbH 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-cache-dynamodb 2 | 3 | [![Apache 2.0 License][license-image]][license-url] 4 | [![Build Status](https://www.travis-ci.com/das-buro-am-draht/spring-cache-dynamodb.svg?branch=master)](https://www.travis-ci.com/das-buro-am-draht/spring-cache-dynamodb) 5 | [![Coverage Status](https://coveralls.io/repos/github/das-buro-am-draht/spring-cache-dynamodb/badge.svg)](https://coveralls.io/github/das-buro-am-draht/spring-cache-dynamodb) 6 | 7 | > Spring Cache implementation based on Amazon DynamoDB 8 | 9 | ## Install 10 | 11 | To integrate this Git repository into your project, simply add the maven dependency 12 | 13 | ```xml 14 | 15 | com.dasburo 16 | spring-cache-dynamodb 17 | 2.0.0 18 | 19 | ``` 20 | This release is using the AWS Java SDK v2.x. If you must use v1.x, please use a version prior to 2.0.0 of this library. 21 | 22 | ## Usage 23 | 24 | ### Quick start 25 | 26 | There is an autoconfiguration class that will set up a simple key-value cache for you, provided you have specified the following properties. 27 | 28 | #### Properties 29 | 30 | ```properties 31 | # TTL (in seconds). Default is Duration.ZERO and disables TTL. 32 | spring.cache.dynamo.caches[0].ttl = 10s 33 | 34 | # Cache name for the @Cacheable annotation. 35 | spring.cache.dynamo.caches[0].cacheName = myCache 36 | 37 | # Value that indicates if the cache must be flushed on application start. 38 | spring.cache.dynamo.caches[0].flushOnBoot = true 39 | ``` 40 | 41 | #### YAML 42 | 43 | ```yaml 44 | spring: 45 | cache: 46 | dynamo: 47 | caches: 48 | - # TTL. 49 | ttl: 10s 50 | # Cache name for the @Cacheable annotation. 51 | cacheName: myCache 52 | # Value that indicates if the cache table must be flushed when the application starts. 53 | flushOnBoot: true 54 | ``` 55 | 56 | ### Custom configuration 57 | 58 | To customize the creation of a cache manager, create a Java Bean in a [configuration class](http://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-configuration-classes.html): 59 | 60 | ```java 61 | @Bean 62 | public AwsCredentials awsCredentials() { 63 | return AwsBasicCredentials.create(amazonAWSAccessKey, amazonAWSSecretKey); 64 | } 65 | 66 | @Bean 67 | public AwsCredentialsProvider awsCredentialsProvider(AwsCredentials awsCredentials) { 68 | return StaticCredentialsProvider.create(awsCredentials); 69 | } 70 | 71 | @Bean 72 | public DynamoDbClient amazonDynamoDB(AwsCredentialsProvider awsCredentialsProvider) { 73 | return DynamoDbClient.builder() 74 | .credentialsProvider(awsCredentialsProvider) 75 | .region(Region.of(amazonAWSRegion)) 76 | .build(); 77 | } 78 | 79 | @Bean 80 | public CacheManager cacheManager(DynamoDbClient ddb) { 81 | List cacheBuilders = new ArrayList<>(); 82 | cacheBuilders.add(DynamoCacheBuilder.newInstance(cacheName, ddb) 83 | .withTTL(Duration.ofSeconds(cacheTtl))); 84 | 85 | return new DynamoCacheManager(cacheBuilders); 86 | } 87 | ``` 88 | 89 | #### Serializers 90 | 91 | By default, the included `StringSerializer` is used. But it's also possible to define a custom Serializer 92 | of type `DynamoSerializer` for each cache. 93 | 94 | ### How to use the cache? 95 | 96 | #### @Cacheable 97 | 98 | After the cache has been configured, you can use the `Cacheable` [annotation](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html). 99 | In the following example, the cache "myCache" is used like this: 100 | 101 | ```java 102 | @Cacheable(value = "myCache", key = "#id") 103 | public Data getData(String id) { 104 | // ... 105 | } 106 | ``` 107 | 108 | The `id` parameter is used as document identifier. 109 | Note that the cache key must be of type `java.lang.String`. 110 | It is also possible to use [SpEL](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions-beandef-annotation-based) to generate a combined key. 111 | 112 | The `Data` object will be stored in a DynamoDB table for future use (as the TTL has not expired). 113 | Note that cache elements must be serializable (i.e. implement `java.io.Serializable`). 114 | 115 | ## License 116 | 117 | Spring Cache DynamoDB is Open Source software released under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0.html). 118 | 119 | [license-image]: https://img.shields.io/badge/license-Apache%202-blue.svg 120 | [license-url]: http://www.apache.org/licenses/LICENSE-2.0 121 | [travis-image]: https://travis-ci.com/bad-opensource/spring-cache-dynamodb.svg?branch=master 122 | [travis-url]: https://travis-ci.com/bad-opensource/spring-cache-dynamodb 123 | [coveralls-image]: https://coveralls.io/repos/github/bad-opensource/spring-cache-dynamodb/badge.svg 124 | [coveralls-url]: https://coveralls.io/github/bad-opensource/spring-cache-dynamodb 125 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.7.2 9 | 10 | 11 | 12 | com.dasburo 13 | spring-cache-dynamodb 14 | 2.0.0 15 | 16 | Amazon DynamoDB for Spring Cache 17 | Spring Cache implementation based on Amazon DynamoDB 18 | https://github.com/bad-opensource/spring-cache-dynamodb 19 | 2019 20 | 21 | Das Büro am Draht GmbH 22 | https://www.dasburo.com/ 23 | 24 | 25 | 26 | Apache 2.0 License 27 | http://www.apache.org/licenses/LICENSE-2.0 28 | repo 29 | 30 | 31 | 32 | 33 | 34 | derXear 35 | Georg Zimmermann 36 | Das Büro am Draht GmbH 37 | https://www.dasburo.com/ 38 | 39 | Software Developer 40 | 41 | 42 | 43 | 44 | 45 | 46 | scm:git:https://github.com/bad-opensource/spring-cache-dynamodb.git 47 | scm:git:https://github.com/bad-opensource/spring-cache-dynamodb.git 48 | https://github.com/bad-opensource/spring-cache-dynamodb 49 | spring-cache-dynamodb-1.0.0 50 | 51 | 52 | GitHub Issues 53 | https://github.com/bad-opensource/spring-cache-dynamodb/issues 54 | 55 | 56 | 57 | 58 | ossrh 59 | https://oss.sonatype.org/content/repositories/snapshots 60 | 61 | 62 | 63 | 64 | UTF-8 65 | UTF-8 66 | UTF-8 67 | 1.8 68 | 1.8 69 | 1.8 70 | 1.8 71 | github 72 | 73 | 1.16.0 74 | 2.17.253 75 | 76 | 77 | 78 | 79 | 80 | software.amazon.awssdk 81 | bom 82 | ${version.awssdk} 83 | pom 84 | import 85 | 86 | 87 | com.amazonaws 88 | DynamoDBLocal 89 | ${version.dynamodb.local} 90 | test 91 | 92 | 93 | 94 | 95 | 96 | 97 | org.springframework.boot 98 | spring-boot-autoconfigure 99 | 100 | 101 | org.springframework.boot 102 | spring-boot-starter-test 103 | 104 | 105 | org.springframework 106 | spring-tx 107 | 108 | 109 | org.springframework 110 | spring-oxm 111 | 112 | 113 | software.amazon.awssdk 114 | dynamodb 115 | 116 | 117 | 118 | com.fasterxml.jackson.core 119 | jackson-databind 120 | 121 | 122 | commons-beanutils 123 | commons-beanutils 124 | 1.9.4 125 | 126 | 127 | com.amazonaws 128 | DynamoDBLocal 129 | test 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | org.apache.maven.plugins 138 | maven-dependency-plugin 139 | 2.10 140 | 141 | 142 | copy 143 | test-compile 144 | 145 | copy-dependencies 146 | 147 | 148 | test 149 | so,dll,dylib 150 | ${project.basedir}/native-libs 151 | 152 | 153 | 154 | 155 | 156 | org.jacoco 157 | jacoco-maven-plugin 158 | 0.8.8 159 | 160 | 161 | prepare-agent 162 | 163 | prepare-agent 164 | 165 | 166 | 167 | 168 | 169 | org.eluder.coveralls 170 | coveralls-maven-plugin 171 | 4.3.0 172 | 173 | 174 | org.sonatype.plugins 175 | nexus-staging-maven-plugin 176 | 1.6.13 177 | true 178 | 179 | ossrh 180 | https://oss.sonatype.org/ 181 | true 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | ossrh 190 | 191 | true 192 | 193 | 194 | 195 | release 196 | 197 | 198 | 199 | org.apache.maven.plugins 200 | maven-release-plugin 201 | 2.5.3 202 | 203 | true 204 | false 205 | release 206 | deploy 207 | 208 | 209 | 210 | org.apache.maven.plugins 211 | maven-source-plugin 212 | 213 | 214 | attach-sources 215 | 216 | jar-no-fork 217 | 218 | 219 | 220 | 221 | 222 | org.apache.maven.plugins 223 | maven-javadoc-plugin 224 | 225 | 226 | attach-javadocs 227 | 228 | jar 229 | 230 | 231 | 232 | 233 | 234 | org.apache.maven.plugins 235 | maven-gpg-plugin 236 | 1.6 237 | 238 | 239 | sign-artifacts 240 | verify 241 | 242 | sign 243 | 244 | 245 | 246 | --pinentry-mode 247 | loopback 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | dynamodb-local-frankfurt 260 | DynamoDB Local Release Repository 261 | https://s3.eu-central-1.amazonaws.com/dynamodb-local-frankfurt/release 262 | 263 | 264 | 265 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/DefaultDynamoCacheWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dasburo.spring.cache.dynamo; 17 | 18 | import com.dasburo.spring.cache.dynamo.rootattribute.RootAttribute; 19 | import com.dasburo.spring.cache.dynamo.util.TableUtils; 20 | import org.springframework.dao.PessimisticLockingFailureException; 21 | import org.springframework.lang.Nullable; 22 | import org.springframework.util.Assert; 23 | import software.amazon.awssdk.core.SdkBytes; 24 | import software.amazon.awssdk.services.dynamodb.DynamoDbClient; 25 | import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; 26 | import software.amazon.awssdk.services.dynamodb.model.AttributeValue; 27 | import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; 28 | import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; 29 | import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest; 30 | import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; 31 | import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; 32 | import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; 33 | import software.amazon.awssdk.services.dynamodb.model.KeyType; 34 | import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; 35 | import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; 36 | import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; 37 | import software.amazon.awssdk.services.dynamodb.model.TimeToLiveSpecification; 38 | import software.amazon.awssdk.services.dynamodb.model.UpdateTimeToLiveRequest; 39 | 40 | import java.time.Duration; 41 | import java.time.Instant; 42 | import java.util.Collections; 43 | import java.util.HashMap; 44 | import java.util.List; 45 | import java.util.Map; 46 | import java.util.NoSuchElementException; 47 | import java.util.Objects; 48 | import java.util.function.Function; 49 | 50 | import static software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType.S; 51 | 52 | /** 53 | * {@link DynamoCacheWriter} implementation capable of reading/writing binary data from/to DynamoDB in {@literal standalone} 54 | * and {@literal cluster} environments. Works upon a given {@link DynamoDbClient} holds the actual connection. 55 | *

56 | * {@link DefaultDynamoCacheWriter} can be used in 57 | * {@link DynamoCacheWriter#lockingDynamoCacheWriter(DynamoDbClient) locking} or 58 | * {@link DynamoCacheWriter#nonLockingDynamoCacheWriter(DynamoDbClient) non-locking} mode. While 59 | * {@literal non-locking} aims for maximum performance it may result in overlapping, non-atomic, command execution for 60 | * operations spanning multiple DynamoDB interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents 61 | * command overlap by setting an explicit lock key and checking against presence of this key which leads to additional 62 | * requests and potential command wait times. 63 | * 64 | * @author Georg Zimmermann 65 | */ 66 | public class DefaultDynamoCacheWriter implements DynamoCacheWriter { 67 | 68 | public static final String ATTRIBUTE_KEY = "key"; 69 | public static final String ATTRIBUTE_VALUE = "value"; 70 | public static final String ATTRIBUTE_TTL = "ttl"; 71 | 72 | private final DynamoDbClient dynamoTemplate; 73 | private final Duration sleepTime; 74 | 75 | /** 76 | * @param dynamoTemplate must not be {@literal null}. 77 | */ 78 | DefaultDynamoCacheWriter(DynamoDbClient dynamoTemplate) { 79 | this(dynamoTemplate, Duration.ZERO); 80 | } 81 | 82 | /** 83 | * @param dynamoTemplate must not be {@literal null}. 84 | * @param sleepTime sleep time between lock request attempts. Must not be {@literal null}. Use {@link Duration#ZERO} 85 | * to disable locking. 86 | */ 87 | DefaultDynamoCacheWriter(DynamoDbClient dynamoTemplate, Duration sleepTime) { 88 | Assert.notNull(dynamoTemplate, "ConnectionFactory must not be null!"); 89 | Assert.notNull(sleepTime, "SleepTime must not be null!"); 90 | 91 | this.dynamoTemplate = dynamoTemplate; 92 | this.sleepTime = sleepTime; 93 | } 94 | 95 | @Override 96 | public DynamoDbClient getNativeCacheWriter() { 97 | return dynamoTemplate; 98 | } 99 | 100 | @Override 101 | public void put(String name, String key, byte[] value, @Nullable Duration ttl, @Nullable List rootAttributes) { 102 | Assert.notNull(name, "Name must not be null!"); 103 | Assert.notNull(key, "Key must not be null!"); 104 | 105 | execute(name, connection -> { 106 | putInternal(name, key, value, ttl, rootAttributes); 107 | 108 | return "OK"; 109 | }); 110 | } 111 | 112 | @Override 113 | public byte[] get(String name, String key) { 114 | Assert.notNull(name, "Name must not be null!"); 115 | Assert.notNull(key, "Key must not be null!"); 116 | 117 | return execute(name, connection -> getInternal(name, key)); 118 | } 119 | 120 | @Override 121 | public byte[] putIfAbsent(String name, String key, @Nullable byte[] value, @Nullable Duration ttl, @Nullable List rootAttributes) { 122 | Assert.notNull(name, "Name must not be null!"); 123 | Assert.notNull(key, "Key must not be null!"); 124 | 125 | return execute(name, connection -> { 126 | 127 | if (isLockingCacheWriter()) { 128 | doLock(name); 129 | } 130 | 131 | try { 132 | return getInternal(name, key); 133 | } catch (NoSuchElementException e) { 134 | putInternal(name, key, value, ttl, rootAttributes); 135 | } finally { 136 | if (isLockingCacheWriter()) { 137 | doUnlock(name); 138 | } 139 | } 140 | return null; 141 | }); 142 | } 143 | 144 | @Override 145 | public void remove(String name, String key) { 146 | Assert.notNull(name, "Name must not be null!"); 147 | Assert.notNull(key, "Key must not be null!"); 148 | 149 | execute(name, connection -> { 150 | removeInternal(name, key); 151 | return "OK"; 152 | }); 153 | } 154 | 155 | @Override 156 | public void clear(String name) { 157 | Assert.notNull(name, "Name must not be null!"); 158 | 159 | execute(name, connection -> { 160 | try { 161 | if (isLockingCacheWriter()) { 162 | doLock(name); 163 | } 164 | 165 | List> items = dynamoTemplate.scan(req -> req.tableName(name)).items(); 166 | 167 | items.parallelStream() 168 | .forEach(map -> { 169 | Map keyToDelete = new HashMap<>(); 170 | keyToDelete.put(ATTRIBUTE_KEY, map.get(ATTRIBUTE_KEY)); 171 | DeleteItemRequest delReq = DeleteItemRequest.builder() 172 | .tableName(name) 173 | .key(keyToDelete) 174 | .build(); 175 | dynamoTemplate.deleteItem(delReq); 176 | }); 177 | } catch (ResourceNotFoundException ignored) { 178 | // ignore table not found 179 | } finally { 180 | if (isLockingCacheWriter()) { 181 | doUnlock(name); 182 | } 183 | } 184 | return "OK"; 185 | }); 186 | } 187 | 188 | @Override 189 | public boolean createIfNotExists(String name, Duration ttl, Long readCapacityUnits, Long writeCapacityUnits) { 190 | Assert.notNull(name, "Name must not be null!"); 191 | Assert.notNull(ttl, "TTL must not be null! Use Duration.ZERO to disable TTL."); 192 | 193 | boolean created = false; 194 | try { 195 | dynamoTemplate.describeTable(DescribeTableRequest.builder() 196 | .tableName(name) 197 | .build()); 198 | } catch (ResourceNotFoundException e) { 199 | created = TableUtils.createTableIfNotExists(dynamoTemplate, createTableRequest(name, readCapacityUnits, writeCapacityUnits)); 200 | if (created && !ttl.isZero()) { 201 | dynamoTemplate.updateTimeToLive(updateTimeToLiveRequest(name)); 202 | } 203 | } 204 | return created; 205 | } 206 | 207 | private byte[] getInternal(String name, String key) { 208 | final GetItemRequest request = GetItemRequest.builder() 209 | .attributesToGet(ATTRIBUTE_VALUE, ATTRIBUTE_TTL) 210 | .tableName(name) 211 | .key(Collections.singletonMap(ATTRIBUTE_KEY, AttributeValue.fromS(key))) 212 | .build(); 213 | 214 | final GetItemResponse result = dynamoTemplate.getItem(request); 215 | if (result.hasItem() && !isPastTtl(result)) { 216 | return getAttributeValue(result); 217 | } else { 218 | throw new NoSuchElementException(String.format("No entry found for '%s'.", key)); 219 | } 220 | } 221 | 222 | private byte[] getAttributeValue(GetItemResponse result) { 223 | final AttributeValue attribute = result.item().get(ATTRIBUTE_VALUE); 224 | if (attribute == null) { 225 | throw new IllegalStateException(String.format("Attribute value does not match the expected '%s'.", ATTRIBUTE_VALUE)); 226 | } 227 | 228 | SdkBytes element = attribute.b(); 229 | if (element == null && attribute.nul()) { 230 | // TODO to return null is bad style, but how to distinct between null value from getInternal and no entry at all? 231 | return null; 232 | } else { 233 | return Objects.requireNonNull(element).asByteArray(); 234 | } 235 | } 236 | 237 | private boolean isPastTtl(GetItemResponse result) { 238 | final AttributeValue attributeTtl = result.item().get(ATTRIBUTE_TTL); 239 | if (attributeTtl != null && attributeTtl.n() != null) { 240 | Instant ttlInstant = Instant.ofEpochSecond(Long.parseLong(attributeTtl.n())); 241 | return Instant.now().isAfter(ttlInstant); 242 | } 243 | return false; 244 | } 245 | 246 | private void putInternal(String name, String key, @Nullable byte[] value, @Nullable Duration ttl, @Nullable List rootAttributes) { 247 | Map attributeValues = new HashMap<>(); 248 | attributeValues.put(ATTRIBUTE_KEY, AttributeValue.fromS(key)); 249 | 250 | if (value == null) { 251 | attributeValues.put(ATTRIBUTE_VALUE, AttributeValue.fromNul(true)); 252 | } else { 253 | attributeValues.put(ATTRIBUTE_VALUE, AttributeValue.fromB(SdkBytes.fromByteArray(value))); 254 | } 255 | 256 | if (shouldExpireWithin(ttl)) { 257 | attributeValues.put(ATTRIBUTE_TTL, AttributeValue.fromN(String.valueOf(Instant.now().plus(ttl).getEpochSecond()))); 258 | } 259 | 260 | if (rootAttributes != null) { 261 | rootAttributes.forEach(rootAttribute -> attributeValues.put(rootAttribute.getName(), rootAttribute.getAttributeValue())); 262 | } 263 | 264 | PutItemRequest putItemRequest = PutItemRequest.builder() 265 | .tableName(name) 266 | .item(attributeValues) 267 | .build(); 268 | dynamoTemplate.putItem(putItemRequest); 269 | } 270 | 271 | private void removeInternal(String name, String key) { 272 | dynamoTemplate.deleteItem(DeleteItemRequest.builder() 273 | .tableName(name) 274 | .key(Collections.singletonMap(ATTRIBUTE_KEY, AttributeValue.fromS(key))) 275 | .build()); 276 | } 277 | 278 | private void doLock(String name) { 279 | // TODO should a ttl be provided for locking? 280 | putInternal(name, createCacheLockKey(name), "1".getBytes(), null, null); 281 | } 282 | 283 | private void doUnlock(String name) { 284 | try { 285 | removeInternal(name, createCacheLockKey(name)); 286 | } catch (ResourceNotFoundException e) { 287 | // ignore 288 | } 289 | } 290 | 291 | private boolean doCheckLock(String name) { 292 | try { 293 | getInternal(name, createCacheLockKey(name)); 294 | } catch (NoSuchElementException | ResourceNotFoundException e) { 295 | return false; 296 | } 297 | return true; 298 | } 299 | 300 | /** 301 | * @return {@literal true} if {@link DynamoCacheWriter} uses locks. 302 | */ 303 | private boolean isLockingCacheWriter() { 304 | return !sleepTime.isZero() && !sleepTime.isNegative(); 305 | } 306 | 307 | private T execute(String name, Function callback) { 308 | checkAndPotentiallyWaitUntilUnlocked(name); 309 | return callback.apply(dynamoTemplate); 310 | } 311 | 312 | private void checkAndPotentiallyWaitUntilUnlocked(String name) { 313 | if (!isLockingCacheWriter()) { 314 | return; 315 | } 316 | 317 | try { 318 | while (doCheckLock(name)) { 319 | Thread.sleep(sleepTime.toMillis()); 320 | } 321 | } catch (InterruptedException ex) { 322 | // Re-interrupt current thread, to allow other participants to react. 323 | Thread.currentThread().interrupt(); 324 | 325 | throw new PessimisticLockingFailureException(String.format("Interrupted while waiting to unlock cache %s", name), 326 | ex); 327 | } 328 | } 329 | 330 | private static boolean shouldExpireWithin(@Nullable Duration ttl) { 331 | return ttl != null && !ttl.isZero() && !ttl.isNegative(); 332 | } 333 | 334 | private static String createCacheLockKey(String name) { 335 | return (name + "~lock"); 336 | } 337 | 338 | private CreateTableRequest createTableRequest(String name, Long readCapacityUnits, Long writeCapacityUnits) { 339 | return CreateTableRequest.builder() 340 | .tableName(name) 341 | .attributeDefinitions(AttributeDefinition.builder() 342 | .attributeName(ATTRIBUTE_KEY) 343 | .attributeType(S) 344 | .build()) 345 | .keySchema(KeySchemaElement.builder() 346 | .attributeName(ATTRIBUTE_KEY) 347 | .keyType(KeyType.HASH) 348 | .build()) 349 | .provisionedThroughput(ProvisionedThroughput.builder() 350 | .readCapacityUnits(readCapacityUnits) 351 | .writeCapacityUnits(writeCapacityUnits) 352 | .build()) 353 | .build(); 354 | } 355 | 356 | // TODO to be tested (not implemented in AmazonDynamoDB local) 357 | private UpdateTimeToLiveRequest updateTimeToLiveRequest(String name) { 358 | return UpdateTimeToLiveRequest.builder() 359 | .tableName(name) 360 | .timeToLiveSpecification(TimeToLiveSpecification.builder() 361 | .enabled(true) 362 | .attributeName(ATTRIBUTE_TTL) 363 | .build()) 364 | .build(); 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/DynamoCache.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dasburo.spring.cache.dynamo; 17 | 18 | import com.dasburo.spring.cache.dynamo.rootattribute.RootAttribute; 19 | import com.dasburo.spring.cache.dynamo.rootattribute.RootAttributeConfig; 20 | import com.dasburo.spring.cache.dynamo.rootattribute.RootAttributeReader; 21 | import com.dasburo.spring.cache.dynamo.serializer.DynamoSerializer; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | import org.springframework.cache.Cache; 25 | import org.springframework.cache.support.SimpleValueWrapper; 26 | import org.springframework.util.Assert; 27 | 28 | import java.time.Duration; 29 | import java.util.List; 30 | import java.util.NoSuchElementException; 31 | import java.util.Objects; 32 | import java.util.concurrent.Callable; 33 | import java.util.stream.Collectors; 34 | 35 | /** 36 | * Spring {@link Cache} adapter implementation 37 | * on top of Amazons DynamoDB. 38 | * 39 | * @author Georg Zimmermann 40 | */ 41 | public class DynamoCache implements Cache { 42 | 43 | private static final Logger LOGGER = LoggerFactory.getLogger(DynamoCache.class); 44 | 45 | private final String cacheName; 46 | private final DynamoCacheWriter writer; 47 | private final DynamoCacheConfiguration cacheConfig; 48 | 49 | private RootAttributeReader rootAttributeReader = new RootAttributeReader(); 50 | 51 | /** 52 | * Constructor. 53 | * 54 | * @param cacheName a cache name. 55 | * @param writer a {@link DynamoCacheWriter} instance. 56 | */ 57 | public DynamoCache(String cacheName, DynamoCacheWriter writer) { 58 | this(cacheName, writer, DynamoCacheConfiguration.defaultCacheConfig()); 59 | } 60 | 61 | /** 62 | * Constructor. 63 | * 64 | * @param cacheName a cache name. 65 | * @param writer a {@link DynamoCacheWriter} instance. 66 | * @param cacheConfig a {@link DynamoCacheConfiguration} instance. Must not be {@literal null}. 67 | */ 68 | public DynamoCache(String cacheName, DynamoCacheWriter writer, DynamoCacheConfiguration cacheConfig) { 69 | Assert.hasText(cacheName, "'cacheName' must be not null and not empty."); 70 | Assert.notNull(writer, "'writer' must not be null."); 71 | 72 | this.cacheName = cacheName; 73 | this.writer = writer; 74 | this.cacheConfig = cacheConfig; 75 | 76 | initialize(); 77 | } 78 | 79 | @Override 80 | public void clear() { 81 | writer.clear(cacheName); 82 | } 83 | 84 | @Override 85 | public void evict(Object key) { 86 | Assert.isTrue(key instanceof String, "'key' must be an instance of 'java.lang.String'."); 87 | 88 | writer.remove(cacheName, (String) key); 89 | } 90 | 91 | @Override 92 | public ValueWrapper get(Object key) { 93 | try { 94 | Object value = getFromCache(key); 95 | return new SimpleValueWrapper(value); 96 | } catch (NoSuchElementException e) { 97 | return null; 98 | } 99 | } 100 | 101 | @Override 102 | public T get(Object key, Class type) { 103 | try { 104 | final Object value = getFromCache(key); 105 | return type.cast(value); 106 | } catch (ClassCastException e) { 107 | throw new IllegalStateException("Unable to cast the object.", e); 108 | } catch (NoSuchElementException e) { 109 | return null; 110 | } 111 | } 112 | 113 | @Override 114 | public T get(Object key, Callable valueLoader) { 115 | Assert.isTrue(key instanceof String, "'key' must be an instance of 'java.lang.String'."); 116 | Assert.notNull(valueLoader, "'valueLoader' must not be null."); 117 | 118 | ValueWrapper cached = get(key); 119 | 120 | if (cached != null) { 121 | return (T) cached.get(); 122 | } 123 | 124 | try { 125 | T value = valueLoader.call(); 126 | put(key, value); 127 | return value; 128 | } catch (Exception e) { 129 | throw new ValueRetrievalException(key, valueLoader, e); 130 | } 131 | } 132 | 133 | /** 134 | * Gets whether the cache should delete all elements on boot. 135 | * 136 | * @return returns whether the cache should delete all elements on boot. 137 | */ 138 | public final boolean isFlushOnBoot() { 139 | return cacheConfig.isFlushOnBoot(); 140 | } 141 | 142 | @Override 143 | public String getName() { 144 | return cacheName; 145 | } 146 | 147 | @Override 148 | public Object getNativeCache() { 149 | return writer.getNativeCacheWriter(); 150 | } 151 | 152 | /** 153 | * Returns the time to live value for this cache. 154 | * 155 | * @return the ttl value. 156 | */ 157 | public final Duration getTtl() { 158 | return cacheConfig.getTtl(); 159 | } 160 | 161 | /** 162 | * Returns the configuration of additional root attributes for this cache 163 | * 164 | * @return the rootAttributeConfigs value. 165 | */ 166 | public final List getRootAttributes() { 167 | return cacheConfig.getRootAttributes(); 168 | } 169 | 170 | /** 171 | * Returns the implementation of {@link DynamoCacheWriter} used for caching 172 | * 173 | * @return the DynamoCacheWriter implementation. 174 | */ 175 | public final DynamoCacheWriter getWriter() { 176 | return writer; 177 | } 178 | 179 | /** 180 | * Returns the implementation of {@link DynamoSerializer} used to serialize the value to be cached 181 | * 182 | * @return the DynamoSerializer implementation. 183 | */ 184 | public final DynamoSerializer getSerializer() { 185 | return cacheConfig.getSerializer(); 186 | } 187 | 188 | @Override 189 | public void put(Object key, Object value) { 190 | Assert.isTrue(key instanceof String, "'key' must be an instance of 'java.lang.String'."); 191 | writer.put(cacheName, (String) key, serialize(value), cacheConfig.getTtl(), readRootAttributes(cacheConfig.getRootAttributes(), value)); 192 | } 193 | 194 | @Override 195 | public ValueWrapper putIfAbsent(Object key, Object value) { 196 | Assert.isTrue(key instanceof String, "'key' must be an instance of 'java.lang.String'."); 197 | 198 | byte[] result = writer.putIfAbsent(cacheName, (String) key, serialize(value), cacheConfig.getTtl(), readRootAttributes(cacheConfig.getRootAttributes(), value)); 199 | if (result != null) { 200 | LOGGER.debug(String.format("Key: %s already exists in the cache. Element will not be replaced.", key)); 201 | return new SimpleValueWrapper(deserialize(result)); 202 | } 203 | 204 | return null; 205 | } 206 | 207 | private Object getFromCache(Object key) { 208 | Assert.isTrue(key instanceof String, "'key' must be an instance of 'java.lang.String'."); 209 | 210 | byte[] element = writer.get(cacheName, (String) key); 211 | return deserialize(element); 212 | } 213 | 214 | private void initialize() { 215 | if (cacheConfig.isFlushOnBoot()) { 216 | clear(); 217 | } 218 | 219 | writer.createIfNotExists(cacheName, cacheConfig.getTtl(), cacheConfig.getReadCapacityUnits(), cacheConfig.getWriteCapacityUnits()); 220 | } 221 | 222 | private Object deserialize(byte[] value) { 223 | return cacheConfig.getSerializer().deserialize(value); 224 | } 225 | 226 | private byte[] serialize(Object value) { 227 | return cacheConfig.getSerializer().serialize(value); 228 | } 229 | 230 | private List readRootAttributes(List rootAttributeConfigs, Object value) { 231 | return rootAttributeConfigs.stream() 232 | .map(rootAttributeConfig -> rootAttributeReader.readRootAttribute(rootAttributeConfig, value)) 233 | .filter(Objects::nonNull) 234 | .collect(Collectors.toList()); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/DynamoCacheBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dasburo.spring.cache.dynamo; 17 | 18 | import com.dasburo.spring.cache.dynamo.rootattribute.RootAttributeConfig; 19 | import com.dasburo.spring.cache.dynamo.serializer.DynamoSerializer; 20 | import org.springframework.util.Assert; 21 | import software.amazon.awssdk.services.dynamodb.DynamoDbClient; 22 | 23 | import java.time.Duration; 24 | import java.util.List; 25 | 26 | /** 27 | * A builder for {@link DynamoCache} instance. 28 | * 29 | * @author Georg Zimmermann 30 | */ 31 | public class DynamoCacheBuilder { 32 | 33 | private String cacheName; 34 | private DynamoCacheWriter writer; 35 | 36 | private DynamoCacheConfiguration cacheConfig; 37 | 38 | /** 39 | * Constructor. 40 | * 41 | * @param cacheName a name of the cache. 42 | * @param dynamoTemplate a {@link DynamoDbClient} instance. 43 | */ 44 | protected DynamoCacheBuilder(final String cacheName, final DynamoDbClient dynamoTemplate) { 45 | Assert.notNull(dynamoTemplate, "'dynamoTemplate' must not be null."); 46 | Assert.hasText(cacheName, "'cacheName' must not be null and must contain at least one non-whitespace character."); 47 | 48 | this.cacheName = cacheName; 49 | this.writer = DynamoCacheWriter.nonLockingDynamoCacheWriter(dynamoTemplate); 50 | this.cacheConfig = DynamoCacheConfiguration.defaultCacheConfig(); 51 | } 52 | 53 | /** 54 | * Create a new builder instance with the given cache name. 55 | * 56 | * @param cacheName a name of the cache. 57 | * @param dynamoTemplate a {@link DynamoDbClient} instance. 58 | * @return a new builder 59 | */ 60 | public static DynamoCacheBuilder newInstance(String cacheName, DynamoDbClient dynamoTemplate) { 61 | return new DynamoCacheBuilder(cacheName, dynamoTemplate); 62 | } 63 | 64 | /** 65 | * Build a new {@link DynamoCache} with the specified name. 66 | * 67 | * @return a {@link DynamoCache} instance. 68 | */ 69 | public DynamoCache build() { 70 | return new DynamoCache(cacheName, writer, cacheConfig); 71 | } 72 | 73 | /** 74 | * Give a value that indicates if the collection must be always flush. 75 | * 76 | * @param flushOnBoot a value that indicates if the collection must be always flush. 77 | * @return this builder for chaining. 78 | */ 79 | public DynamoCacheBuilder withFlushOnBoot(boolean flushOnBoot) { 80 | cacheConfig.setFlushOnBoot(flushOnBoot); 81 | return this; 82 | } 83 | 84 | /** 85 | * Give a TTL to the cache to be built. 86 | * 87 | * @param ttl a time-to-live (in seconds). 88 | * @return this builder for chaining. 89 | */ 90 | public DynamoCacheBuilder withTTL(Duration ttl) { 91 | cacheConfig.setTtl(ttl); 92 | return this; 93 | } 94 | 95 | /** 96 | * Give a value that indicates the reads per second to be built. 97 | * 98 | * @param readCapacityUnit amount of strongly consistent reads per second. 99 | * @return this builder for chaining. 100 | */ 101 | public DynamoCacheBuilder withReadCapacityUnit(Long readCapacityUnit) { 102 | cacheConfig.setReadCapacityUnits(readCapacityUnit); 103 | return this; 104 | } 105 | 106 | /** 107 | * Give a value that indicates the writes per second to be built. 108 | * 109 | * @param writeCapacityUnit amount of strongly consistent reads per second. 110 | * @return this builder for chaining. 111 | */ 112 | public DynamoCacheBuilder withWriteCapacityUnit(Long writeCapacityUnit) { 113 | cacheConfig.setWriteCapacityUnits(writeCapacityUnit); 114 | return this; 115 | } 116 | 117 | /** 118 | * Give a {@link DynamoSerializer} to the cache to be built. 119 | * Defaults to {@link com.dasburo.spring.cache.dynamo.serializer.StringSerializer}. 120 | * 121 | * @param serializer a serializer to serialize/deserialize the data 122 | * @return this builder for chaining. 123 | */ 124 | public DynamoCacheBuilder withSerializer(DynamoSerializer serializer) { 125 | cacheConfig.setSerializer(serializer); 126 | return this; 127 | } 128 | 129 | /** 130 | * Give a {@link RootAttributeConfig} to the cache to be built. 131 | * Defaults to empty {@link List}. 132 | * 133 | * @param rootAttributeConfigs additional attributes written into the root level 134 | * @return this builder for chaining. 135 | */ 136 | public DynamoCacheBuilder withRootAttributes(List rootAttributeConfigs) { 137 | this.cacheConfig.setRootAttributes(rootAttributeConfigs); 138 | return this; 139 | } 140 | 141 | /** 142 | * Give a {@link DynamoCacheWriter} to the cache to be built. 143 | * Defaults to {@link DefaultDynamoCacheWriter}. 144 | * 145 | * @param writer a writer doing the actual cache operations. 146 | * @return this builder for chaining. 147 | */ 148 | public DynamoCacheBuilder withWriter(DynamoCacheWriter writer) { 149 | this.writer = writer; 150 | return this; 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/DynamoCacheConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo; 16 | 17 | import com.dasburo.spring.cache.dynamo.rootattribute.RootAttributeConfig; 18 | import com.dasburo.spring.cache.dynamo.serializer.DynamoSerializer; 19 | import com.dasburo.spring.cache.dynamo.serializer.StringSerializer; 20 | 21 | import java.time.Duration; 22 | import java.util.List; 23 | 24 | import static java.util.Collections.emptyList; 25 | 26 | /** 27 | * A configuration container for a {@link DynamoCache} instance. 28 | * 29 | * @author Georg Zimmermann 30 | */ 31 | public class DynamoCacheConfiguration { 32 | 33 | private Duration ttl; 34 | private boolean flushOnBoot; 35 | private Long readCapacityUnits; 36 | private Long writeCapacityUnits; 37 | private DynamoSerializer serializer; 38 | private List rootAttributes; 39 | 40 | private DynamoCacheConfiguration(Duration ttl, boolean flushOnBoot, Long readCapacityUnits, Long writeCapacityUnits, DynamoSerializer serializer, List rootAttributes) { 41 | this.ttl = ttl; 42 | this.flushOnBoot = flushOnBoot; 43 | this.readCapacityUnits = readCapacityUnits; 44 | this.writeCapacityUnits = writeCapacityUnits; 45 | this.serializer = serializer; 46 | this.rootAttributes = rootAttributes; 47 | } 48 | 49 | public static DynamoCacheConfiguration defaultCacheConfig() { 50 | return new DynamoCacheConfiguration(Duration.ZERO, false, 1L, 1L, new StringSerializer(), emptyList()); 51 | } 52 | 53 | public Duration getTtl() { 54 | return ttl; 55 | } 56 | 57 | public void setTtl(Duration ttl) { 58 | this.ttl = ttl; 59 | } 60 | 61 | public boolean isFlushOnBoot() { 62 | return flushOnBoot; 63 | } 64 | 65 | public void setFlushOnBoot(boolean flushOnBoot) { 66 | this.flushOnBoot = flushOnBoot; 67 | } 68 | 69 | public Long getReadCapacityUnits() { 70 | return readCapacityUnits; 71 | } 72 | 73 | public void setReadCapacityUnits(Long readCapacityUnits) { 74 | this.readCapacityUnits = readCapacityUnits; 75 | } 76 | 77 | public Long getWriteCapacityUnits() { 78 | return writeCapacityUnits; 79 | } 80 | 81 | public void setWriteCapacityUnits(Long writeCapacityUnits) { 82 | this.writeCapacityUnits = writeCapacityUnits; 83 | } 84 | 85 | public DynamoSerializer getSerializer() { 86 | return serializer; 87 | } 88 | 89 | public void setSerializer(DynamoSerializer serializer) { 90 | this.serializer = serializer; 91 | } 92 | 93 | public List getRootAttributes() { 94 | return rootAttributes; 95 | } 96 | 97 | public void setRootAttributes(List rootAttributes) { 98 | this.rootAttributes = rootAttributes; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/DynamoCacheManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dasburo.spring.cache.dynamo; 17 | 18 | import org.springframework.cache.Cache; 19 | import org.springframework.cache.support.AbstractCacheManager; 20 | import org.springframework.util.Assert; 21 | 22 | import java.util.ArrayList; 23 | import java.util.Collection; 24 | import java.util.LinkedHashSet; 25 | 26 | /** 27 | * CacheManager implementation that lazily builds {@link DynamoCache} 28 | * instances for each {@link #getCache} request. 29 | * 30 | * @author Georg Zimmermann 31 | */ 32 | public class DynamoCacheManager extends AbstractCacheManager { 33 | 34 | private final Collection initialCaches; 35 | 36 | /** 37 | * Constructor. 38 | * 39 | * @param initialCaches the caches to make available on startup. 40 | */ 41 | public DynamoCacheManager(final Collection initialCaches) { 42 | Assert.notEmpty(initialCaches, "At least one cache builder must be specified."); 43 | this.initialCaches = new ArrayList<>(initialCaches); 44 | } 45 | 46 | @Override 47 | protected Collection loadCaches() { 48 | final Collection caches = new LinkedHashSet<>(initialCaches.size()); 49 | for (final DynamoCacheBuilder cacheBuilder : initialCaches) { 50 | final DynamoCache cache = cacheBuilder.build(); 51 | caches.add(cache); 52 | } 53 | 54 | return caches; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/DynamoCacheWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dasburo.spring.cache.dynamo; 17 | 18 | import com.dasburo.spring.cache.dynamo.rootattribute.RootAttribute; 19 | import org.springframework.lang.Nullable; 20 | import org.springframework.util.Assert; 21 | import software.amazon.awssdk.services.dynamodb.DynamoDbClient; 22 | 23 | import java.time.Duration; 24 | import java.util.List; 25 | 26 | /** 27 | * {@link DynamoCacheWriter} provides low level access to DynamoDB commands ({@code PUT, GET, ...}) used for 28 | * caching. 29 | *

30 | * The {@link DynamoCacheWriter} is responsible for writing / reading binary data to / from DynamoDB. 31 | * 32 | * @author Georg Zimmermann 33 | */ 34 | public interface DynamoCacheWriter { 35 | 36 | /** 37 | * Create new {@link DynamoCacheWriter} without locking behavior. 38 | * 39 | * @return new instance of {@link DefaultDynamoCacheWriter}. 40 | */ 41 | static DynamoCacheWriter nonLockingDynamoCacheWriter(DynamoDbClient dynamoTemplate) { 42 | 43 | Assert.notNull(dynamoTemplate, "AmazonDynamoDB must not be null!"); 44 | 45 | return new DefaultDynamoCacheWriter(dynamoTemplate); 46 | } 47 | 48 | /** 49 | * Create new {@link DynamoCacheWriter} with locking behavior. 50 | * 51 | * @param dynamoTemplate must not be {@literal null}. 52 | * @return new instance of {@link DefaultDynamoCacheWriter}. 53 | */ 54 | static DynamoCacheWriter lockingDynamoCacheWriter(DynamoDbClient dynamoTemplate) { 55 | 56 | Assert.notNull(dynamoTemplate, "AmazonDynamoDB must not be null!"); 57 | 58 | return new DefaultDynamoCacheWriter(dynamoTemplate, Duration.ofMillis(50)); 59 | } 60 | 61 | /** 62 | * Returns the native connection library for the cache. 63 | * 64 | * @return {@link DynamoDbClient} 65 | */ 66 | DynamoDbClient getNativeCacheWriter(); 67 | 68 | /** 69 | * Create a cache table for the given name. 70 | * 71 | * @param name The cache name must not be {@literal null}. 72 | * @param ttl Optional expiration time. Must not be {@literal null}. Use {@code Duration.ZERO} to declare an eternal cache. 73 | * @param readCapacityUnits Amount of strongly consistent reads per second must not be {@literal null}. 74 | * @param writeCapacityUnits Amount of strongly consistent writes per second must not be {@literal null}. 75 | * @return {@literal true} if table had to be created. 76 | */ 77 | boolean createIfNotExists(String name, Duration ttl, Long readCapacityUnits, Long writeCapacityUnits); 78 | 79 | /** 80 | * Write the given key/value pair to Dynamo and set the expiration time if defined. 81 | *
Note: The values size must be less than 400 KB. 82 | * As this is the maximum element size of a binary in Amazons DynamoDB. 83 | * 84 | * @param name The cache name must not be {@literal null}. 85 | * @param key The key for the cache entry. Must not be {@literal null}. 86 | * @param value The value stored for the key. Must not be {@literal null}. 87 | * @param ttl Optional expiration time. Can be {@literal null}. 88 | * @param rootAttributes Optional additional root attributes. Can be {@literal null}. 89 | */ 90 | void put(String name, String key, byte[] value, @Nullable Duration ttl, @Nullable List rootAttributes); 91 | 92 | /** 93 | * Get the binary value representation from Dynamo stored for the given key. 94 | * 95 | * @param name must not be {@literal null}. 96 | * @param key must not be {@literal null}. 97 | * @return {@literal null} if key does not exist. 98 | */ 99 | @Nullable 100 | byte[] get(String name, String key); 101 | 102 | /** 103 | * Write the given value to Dynamo if the key does not already exist. 104 | *
Note: The values size must be less than 400 KB. 105 | * As this is the maximum element size of a binary in Amazons DynamoDB. 106 | * 107 | * @param name The cache name must not be {@literal null}. 108 | * @param key The key for the cache entry. Must not be {@literal null}. 109 | * @param value The value stored for the key. Must not be {@literal null}. 110 | * @param ttl Optional expiration time. Can be {@literal null}. 111 | * @param rootAttributes Optional additional root attributes. Can be {@literal null}. 112 | * @return {@literal null} if the value has been written, the value stored for the key if it already exists. 113 | */ 114 | @Nullable 115 | byte[] putIfAbsent(String name, String key, byte[] value, @Nullable Duration ttl, @Nullable List rootAttributes); 116 | 117 | /** 118 | * Remove the given key from Dynamo. 119 | * 120 | * @param name The cache name must not be {@literal null}. 121 | * @param key The key for the cache entry. Must not be {@literal null}. 122 | */ 123 | void remove(String name, String key); 124 | 125 | /** 126 | * Remove all keys from the given cache name. 127 | *
Note: Clear is actually a table scan followed by per Item deletion. 128 | * This could lead to performance issues on very large data sets. 129 | * 130 | * @param name The cache name must not be {@literal null}. 131 | */ 132 | void clear(String name); 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/autoconfigure/DynamoCacheAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.autoconfigure; 16 | 17 | import com.dasburo.spring.cache.dynamo.DynamoCacheBuilder; 18 | import com.dasburo.spring.cache.dynamo.DynamoCacheManager; 19 | import com.dasburo.spring.cache.dynamo.DynamoCacheWriter; 20 | import com.dasburo.spring.cache.dynamo.serializer.StringSerializer; 21 | import org.springframework.beans.factory.annotation.Autowired; 22 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 23 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 24 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 25 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 26 | import org.springframework.cache.CacheManager; 27 | import org.springframework.context.annotation.Bean; 28 | import org.springframework.context.annotation.Configuration; 29 | import software.amazon.awssdk.services.dynamodb.DynamoDbClient; 30 | 31 | import java.util.ArrayList; 32 | import java.util.List; 33 | 34 | /** 35 | * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration Auto configuration} for {@code DynamoCacheManager} support. 36 | * 37 | * @author Georg Zimmermann 38 | */ 39 | @Configuration 40 | @ConditionalOnClass(DynamoDbClient.class) 41 | @ConditionalOnMissingBean(CacheManager.class) 42 | @EnableConfigurationProperties(DynamoCachePropertiesList.class) 43 | public class DynamoCacheAutoConfiguration { 44 | 45 | private final DynamoDbClient dynamoTemplate; 46 | 47 | private final DynamoCachePropertiesList properties; 48 | 49 | @Autowired 50 | public DynamoCacheAutoConfiguration(DynamoDbClient dynamoTemplate, DynamoCachePropertiesList properties) { 51 | this.dynamoTemplate = dynamoTemplate; 52 | this.properties = properties; 53 | } 54 | 55 | /** 56 | * Creates an instance of the {@code CacheManager} class. 57 | * Only instantiates if there is at least one cache defined. 58 | * 59 | * @return the instance of {@code CacheManager} class. 60 | */ 61 | @Bean 62 | @ConditionalOnProperty("spring.cache.dynamo.caches[0].cacheName") 63 | public CacheManager dynamoCacheManager() { 64 | return new DynamoCacheManager(dynamoCacheBuilders()); 65 | } 66 | 67 | private List dynamoCacheBuilders() { 68 | 69 | List builders = new ArrayList<>(); 70 | 71 | if (properties.getCaches() != null) { 72 | for (DynamoCacheProperties dynamoCacheProperties : properties.getCaches()) { 73 | builders.add( 74 | DynamoCacheBuilder 75 | .newInstance( 76 | dynamoCacheProperties.getCacheName(), dynamoTemplate 77 | ) 78 | .withTTL(dynamoCacheProperties.getTtl()) 79 | .withFlushOnBoot(dynamoCacheProperties.isFlushOnBoot()) 80 | .withReadCapacityUnit(dynamoCacheProperties.getReadCapacityUnits()) 81 | .withWriteCapacityUnit(dynamoCacheProperties.getWriteCapacityUnits()) 82 | .withSerializer(new StringSerializer()) 83 | .withRootAttributes(dynamoCacheProperties.getRootAttributes()) 84 | .withWriter(DynamoCacheWriter.nonLockingDynamoCacheWriter(dynamoTemplate)) 85 | ); 86 | } 87 | } 88 | 89 | return builders; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/autoconfigure/DynamoCacheProperties.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.autoconfigure; 16 | 17 | import com.dasburo.spring.cache.dynamo.DynamoCache; 18 | import com.dasburo.spring.cache.dynamo.rootattribute.RootAttributeConfig; 19 | 20 | import java.time.Duration; 21 | import java.util.List; 22 | 23 | /** 24 | * Properties for {@link DynamoCache}. 25 | * 26 | * @author Georg Zimmermann 27 | */ 28 | public class DynamoCacheProperties { 29 | 30 | private String cacheName; 31 | private boolean flushOnBoot; 32 | private Duration ttl; 33 | private List rootAttributes; 34 | private Long readCapacityUnits = 1L; 35 | private Long writeCapacityUnits = 1L; 36 | 37 | public String getCacheName() { 38 | return cacheName; 39 | } 40 | 41 | public void setCacheName(String cacheName) { 42 | this.cacheName = cacheName; 43 | } 44 | 45 | public boolean isFlushOnBoot() { 46 | return flushOnBoot; 47 | } 48 | 49 | public void setFlushOnBoot(boolean flushOnBoot) { 50 | this.flushOnBoot = flushOnBoot; 51 | } 52 | 53 | public Duration getTtl() { 54 | return ttl; 55 | } 56 | 57 | public void setTtl(Duration ttl) { 58 | this.ttl = ttl; 59 | } 60 | 61 | public List getRootAttributes() { 62 | return rootAttributes; 63 | } 64 | 65 | public void setRootAttributes(List rootAttributes) { 66 | this.rootAttributes = rootAttributes; 67 | } 68 | 69 | public Long getReadCapacityUnits() { 70 | return readCapacityUnits; 71 | } 72 | 73 | public void setReadCapacityUnits(Long readCapacityUnits) { 74 | this.readCapacityUnits = readCapacityUnits; 75 | } 76 | 77 | public Long getWriteCapacityUnits() { 78 | return writeCapacityUnits; 79 | } 80 | 81 | public void setWriteCapacityUnits(Long writeCapacityUnits) { 82 | this.writeCapacityUnits = writeCapacityUnits; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/autoconfigure/DynamoCachePropertiesList.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.autoconfigure; 16 | 17 | import com.dasburo.spring.cache.dynamo.DynamoCache; 18 | import org.springframework.boot.context.properties.ConfigurationProperties; 19 | 20 | import java.util.List; 21 | 22 | /** 23 | * Properties for {@link DynamoCache}. 24 | * 25 | * @author Georg Zimmermann 26 | */ 27 | @ConfigurationProperties(prefix = "spring.cache.dynamo") 28 | public class DynamoCachePropertiesList { 29 | 30 | private List caches; 31 | 32 | public List getCaches() { 33 | return caches; 34 | } 35 | 36 | public void setCaches(List caches) { 37 | this.caches = caches; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/rootattribute/RootAttribute.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dasburo.spring.cache.dynamo.rootattribute; 17 | 18 | import software.amazon.awssdk.services.dynamodb.model.AttributeValue; 19 | 20 | public class RootAttribute { 21 | 22 | private String name; 23 | private AttributeValue value; 24 | 25 | public RootAttribute(String name, AttributeValue value) { 26 | this.name = name; 27 | this.value = value; 28 | } 29 | 30 | public String getName() { 31 | return name; 32 | } 33 | 34 | public AttributeValue getAttributeValue() { 35 | return value; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/rootattribute/RootAttributeConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dasburo.spring.cache.dynamo.rootattribute; 17 | 18 | import org.springframework.util.Assert; 19 | import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; 20 | 21 | import static com.dasburo.spring.cache.dynamo.DefaultDynamoCacheWriter.ATTRIBUTE_KEY; 22 | import static com.dasburo.spring.cache.dynamo.DefaultDynamoCacheWriter.ATTRIBUTE_TTL; 23 | import static com.dasburo.spring.cache.dynamo.DefaultDynamoCacheWriter.ATTRIBUTE_VALUE; 24 | 25 | public class RootAttributeConfig { 26 | 27 | private String name; 28 | private ScalarAttributeType type; 29 | 30 | /* default constructor required for property binding */ 31 | public RootAttributeConfig(){ 32 | } 33 | 34 | public RootAttributeConfig(String name, ScalarAttributeType type) { 35 | setName(name); 36 | setType(type); 37 | } 38 | 39 | public String getName() { 40 | return name; 41 | } 42 | 43 | public void setName(String name) { 44 | Assert.notNull(name, "name must not be null!"); 45 | Assert.isTrue(name.length() > 0, "name must not be empty!"); 46 | Assert.isTrue(!ATTRIBUTE_KEY.equalsIgnoreCase(name), "name must not equal '" + ATTRIBUTE_KEY+"'"); 47 | Assert.isTrue(!ATTRIBUTE_VALUE.equalsIgnoreCase(name), "name must not equal '" + ATTRIBUTE_VALUE+"'"); 48 | Assert.isTrue(!ATTRIBUTE_TTL.equalsIgnoreCase(name), "name must not equal '" + ATTRIBUTE_TTL+"'"); 49 | this.name = name; 50 | } 51 | 52 | public ScalarAttributeType getType() { 53 | return type; 54 | } 55 | 56 | public void setType(ScalarAttributeType type) { 57 | Assert.notNull(type, "type must not be null!"); 58 | this.type = type; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/rootattribute/RootAttributeReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dasburo.spring.cache.dynamo.rootattribute; 17 | 18 | import org.apache.commons.beanutils.PropertyUtils; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | import org.springframework.lang.NonNull; 22 | import org.springframework.lang.Nullable; 23 | import software.amazon.awssdk.core.SdkBytes; 24 | import software.amazon.awssdk.services.dynamodb.model.AttributeValue; 25 | import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; 26 | 27 | import java.lang.reflect.InvocationTargetException; 28 | import java.nio.ByteBuffer; 29 | 30 | public class RootAttributeReader { 31 | 32 | private static final Logger LOGGER = LoggerFactory.getLogger(RootAttributeReader.class); 33 | 34 | @Nullable 35 | public RootAttribute readRootAttribute(@NonNull RootAttributeConfig rootAttributeConfig, @NonNull Object object) { 36 | try { 37 | Object value = PropertyUtils.getProperty(object, rootAttributeConfig.getName()); 38 | if (value == null) { 39 | return null; 40 | } 41 | AttributeValue attributeValue = mapValueToAttributeValue(value, rootAttributeConfig.getType()); 42 | return new RootAttribute(rootAttributeConfig.getName(), attributeValue); 43 | } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { 44 | LOGGER.trace("Unable to access attribute {} on instance of class {}", rootAttributeConfig.getName(), object.getClass()); 45 | return null; 46 | } 47 | } 48 | 49 | @Nullable 50 | private AttributeValue mapValueToAttributeValue(@NonNull Object value, @NonNull ScalarAttributeType type) { 51 | switch (type) { 52 | case N: 53 | return AttributeValue.fromN(String.valueOf(value)); 54 | case B: 55 | return AttributeValue.fromB(SdkBytes.fromByteBuffer((ByteBuffer) value)); 56 | case S: 57 | default: 58 | return AttributeValue.fromS(String.valueOf(value)); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/serializer/DynamoSerializer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.serializer; 16 | 17 | import org.springframework.lang.Nullable; 18 | import org.springframework.util.ClassUtils; 19 | 20 | public interface DynamoSerializer { 21 | 22 | /** 23 | * Serialize the given object to binary data. 24 | * 25 | * @param t object to serialize. Can be {@literal null}. 26 | * @return the equivalent binary data. Can be {@literal null}. 27 | */ 28 | @Nullable 29 | byte[] serialize(@Nullable T t) throws SerializationException; 30 | 31 | /** 32 | * Deserialize an object from the given binary data. 33 | * 34 | * @param bytes object binary representation. Can be {@literal null}. 35 | * @return the equivalent object instance. Can be {@literal null}. 36 | */ 37 | @Nullable 38 | T deserialize(@Nullable byte[] bytes) throws SerializationException; 39 | 40 | default boolean canSerialize(Class type) { 41 | return ClassUtils.isAssignable(getTargetType(), type); 42 | } 43 | 44 | default Class getTargetType() { 45 | return Object.class; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/serializer/GZipSerializer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.serializer; 16 | 17 | import org.springframework.beans.factory.InitializingBean; 18 | import org.springframework.lang.Nullable; 19 | import org.springframework.util.Assert; 20 | 21 | import java.io.ByteArrayInputStream; 22 | import java.io.ByteArrayOutputStream; 23 | import java.io.IOException; 24 | import java.util.zip.GZIPInputStream; 25 | import java.util.zip.GZIPOutputStream; 26 | 27 | /** 28 | * {@link DynamoSerializer} that can read and write the given object with the parent serializer 29 | * and compress or uncompress the resulting data using {@link GZIPInputStream} 30 | * 31 | * @author Georg Zimmermann 32 | */ 33 | public class GZipSerializer implements InitializingBean, DynamoSerializer { 34 | 35 | private @Nullable 36 | DynamoSerializer parent; 37 | 38 | /** 39 | * Creates a new {@link GZipSerializer} for the given class 40 | * 41 | * @param parent the parent {@link DynamoSerializer} 42 | */ 43 | public GZipSerializer(DynamoSerializer parent) { 44 | setParent(parent); 45 | } 46 | 47 | /** 48 | * @param parent The parent serializer to set. 49 | */ 50 | public void setParent(DynamoSerializer parent) { 51 | Assert.notNull(parent, "Parent serializer must not be null!"); 52 | 53 | this.parent = parent; 54 | } 55 | 56 | @Override 57 | public byte[] serialize(@Nullable T t) throws SerializationException { 58 | byte[] data = parent.serialize(t); 59 | 60 | if (SerializationUtils.isEmpty(data)) { 61 | return null; 62 | } 63 | 64 | ByteArrayOutputStream out = new ByteArrayOutputStream(data.length); 65 | 66 | try (GZIPOutputStream zipOut = new GZIPOutputStream(out)) { 67 | zipOut.write(data); 68 | } catch (IOException e) { 69 | throw new SerializationException("Failed to zip value.", e); 70 | } 71 | 72 | return out.toByteArray(); 73 | } 74 | 75 | @Override 76 | public T deserialize(@Nullable byte[] zippedBytes) throws SerializationException { 77 | 78 | if (SerializationUtils.isEmpty(zippedBytes)) { 79 | return null; 80 | } 81 | 82 | ByteArrayInputStream bais = new ByteArrayInputStream(zippedBytes); 83 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 84 | 85 | try (GZIPInputStream is = new GZIPInputStream(bais)) { 86 | int chunkSize = 1024; 87 | byte[] buffer = new byte[chunkSize]; 88 | int length = 0; 89 | while ((length = is.read(buffer, 0, chunkSize)) != -1) { 90 | baos.write(buffer, 0, length); 91 | } 92 | 93 | baos.close(); 94 | bais.close(); 95 | 96 | return parent.deserialize(baos.toByteArray()); 97 | } catch (IOException e) { 98 | throw new SerializationException("Failed to unzip value.", e); 99 | } 100 | } 101 | 102 | /* 103 | * (non-Javadoc) 104 | * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 105 | */ 106 | @Override 107 | public void afterPropertiesSet() { 108 | Assert.notNull(parent, "non-null parent serializer required"); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/serializer/Jackson2JsonSerializer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.serializer; 16 | 17 | import com.fasterxml.jackson.databind.JavaType; 18 | import com.fasterxml.jackson.databind.ObjectMapper; 19 | import com.fasterxml.jackson.databind.ser.SerializerFactory; 20 | import com.fasterxml.jackson.databind.type.TypeFactory; 21 | import org.springframework.lang.Nullable; 22 | import org.springframework.util.Assert; 23 | 24 | /** 25 | * {@link DynamoSerializer} that can read and write JSON using 26 | * Jackson's and 27 | * Jackson Databind {@link ObjectMapper}. 28 | *

29 | * This converter can be used to bind to typed beans, or untyped {@link java.util.HashMap HashMap} instances. 30 | * Note: 31 | * Does not perform any {@literal null} conversion. 32 | * 33 | * @author Georg Zimmermann 34 | */ 35 | public class Jackson2JsonSerializer implements DynamoSerializer { 36 | 37 | private final JavaType javaType; 38 | 39 | private ObjectMapper objectMapper = new ObjectMapper(); 40 | 41 | /** 42 | * Creates a new {@link Jackson2JsonSerializer} for the given {@link Class}. 43 | * 44 | * @param type given class to be serialized. 45 | */ 46 | public Jackson2JsonSerializer(Class type) { 47 | this.javaType = getJavaType(type); 48 | } 49 | 50 | /** 51 | * Creates a new {@link Jackson2JsonSerializer} for the given {@link JavaType}. 52 | * 53 | * @param javaType 54 | */ 55 | public Jackson2JsonSerializer(JavaType javaType) { 56 | this.javaType = javaType; 57 | } 58 | 59 | @SuppressWarnings("unchecked") 60 | public T deserialize(@Nullable byte[] bytes) throws SerializationException { 61 | 62 | if (SerializationUtils.isEmpty(bytes)) { 63 | return null; 64 | } 65 | try { 66 | return this.objectMapper.readValue(bytes, 0, bytes.length, javaType); 67 | } catch (Exception ex) { 68 | throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex); 69 | } 70 | } 71 | 72 | @Override 73 | public byte[] serialize(@Nullable Object t) throws SerializationException { 74 | 75 | try { 76 | return t == null ? null : this.objectMapper.writeValueAsBytes(t); 77 | } catch (Exception ex) { 78 | throw new SerializationException("Could not write JSON: " + ex.getMessage(), ex); 79 | } 80 | } 81 | 82 | /** 83 | * Sets the {@code ObjectMapper} for this view. If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} 84 | * is used. 85 | *

86 | * Setting a custom-configured {@code ObjectMapper} is one way to take further control of the JSON serialization 87 | * process. For example, an extended {@link SerializerFactory} can be configured that provides custom serializers for 88 | * specific types. The other option for refining the serialization process is to use Jackson's provided annotations on 89 | * the types to be serialized, in which case a custom-configured ObjectMapper is unnecessary. 90 | * 91 | * @param objectMapper a custom {@link ObjectMapper} 92 | */ 93 | public void setObjectMapper(ObjectMapper objectMapper) { 94 | 95 | Assert.notNull(objectMapper, "'objectMapper' must not be null"); 96 | this.objectMapper = objectMapper; 97 | } 98 | 99 | /** 100 | * Returns the Jackson {@link JavaType} for the specific class. 101 | *

102 | * Default implementation returns {@link TypeFactory#constructType(java.lang.reflect.Type)}, but this can be 103 | * overridden in subclasses, to allow for custom generic collection handling. For instance: 104 | * 105 | *

106 |    * protected JavaType getJavaType(Class<?> clazz) {
107 |    * 	if (List.class.isAssignableFrom(clazz)) {
108 |    * 		return TypeFactory.defaultInstance().constructCollectionType(ArrayList.class, MyBean.class);
109 |    *  } else {
110 |    * 		return super.getJavaType(clazz);
111 |    *  }
112 |    * }
113 |    * 
114 | * 115 | * @param clazz the class to return the java type for 116 | * @return the java type 117 | */ 118 | protected JavaType getJavaType(Class clazz) { 119 | return TypeFactory.defaultInstance().constructType(clazz); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/serializer/OxmSerializer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.serializer; 16 | 17 | import org.springframework.beans.factory.InitializingBean; 18 | import org.springframework.lang.Nullable; 19 | import org.springframework.oxm.Marshaller; 20 | import org.springframework.oxm.Unmarshaller; 21 | import org.springframework.util.Assert; 22 | 23 | import javax.xml.transform.stream.StreamResult; 24 | import javax.xml.transform.stream.StreamSource; 25 | import java.io.ByteArrayInputStream; 26 | import java.io.ByteArrayOutputStream; 27 | 28 | /** 29 | * {@link DynamoSerializer} on top of Spring's O/X Mapping. Delegates de-/serialization to O/X mapping {@link Marshaller} 30 | * and {@link Unmarshaller}. 31 | * Note: 32 | * Does not perform any {@literal null} conversion. 33 | * 34 | * @author Georg Zimmermann 35 | */ 36 | public class OxmSerializer implements InitializingBean, DynamoSerializer { 37 | 38 | private @Nullable 39 | Marshaller marshaller; 40 | private @Nullable 41 | Unmarshaller unmarshaller; 42 | 43 | /** 44 | * Creates a new, uninitialized {@link OxmSerializer}. Requires {@link #setMarshaller(Marshaller)} and 45 | * {@link #setUnmarshaller(Unmarshaller)} to be set before this serializer can be used. 46 | */ 47 | public OxmSerializer() { 48 | } 49 | 50 | /** 51 | * Creates a new {@link OxmSerializer} given {@link Marshaller} and {@link Unmarshaller}. 52 | * 53 | * @param marshaller must not be {@literal null}. 54 | * @param unmarshaller must not be {@literal null}. 55 | */ 56 | public OxmSerializer(Marshaller marshaller, Unmarshaller unmarshaller) { 57 | 58 | setMarshaller(marshaller); 59 | setUnmarshaller(unmarshaller); 60 | } 61 | 62 | /** 63 | * @param marshaller The marshaller to set. 64 | */ 65 | public void setMarshaller(Marshaller marshaller) { 66 | Assert.notNull(marshaller, "Marshaller must not be null!"); 67 | 68 | this.marshaller = marshaller; 69 | } 70 | 71 | /** 72 | * @param unmarshaller The unmarshaller to set. 73 | */ 74 | public void setUnmarshaller(Unmarshaller unmarshaller) { 75 | Assert.notNull(unmarshaller, "Unmarshaller must not be null!"); 76 | 77 | this.unmarshaller = unmarshaller; 78 | } 79 | 80 | @Override 81 | public Object deserialize(@Nullable byte[] bytes) throws SerializationException { 82 | 83 | if (SerializationUtils.isEmpty(bytes)) { 84 | return null; 85 | } 86 | 87 | try { 88 | return unmarshaller.unmarshal(new StreamSource(new ByteArrayInputStream(bytes))); 89 | } catch (Exception ex) { 90 | throw new SerializationException("Cannot deserialize bytes", ex); 91 | } 92 | } 93 | 94 | @Override 95 | public byte[] serialize(@Nullable Object t) throws SerializationException { 96 | 97 | if (t == null) { 98 | return null; 99 | } 100 | 101 | ByteArrayOutputStream stream = new ByteArrayOutputStream(); 102 | StreamResult result = new StreamResult(stream); 103 | 104 | try { 105 | marshaller.marshal(t, result); 106 | } catch (Exception ex) { 107 | throw new SerializationException("Cannot serialize object", ex); 108 | } 109 | return stream.toByteArray(); 110 | } 111 | 112 | /* 113 | * (non-Javadoc) 114 | * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 115 | */ 116 | @Override 117 | public void afterPropertiesSet() { 118 | 119 | Assert.notNull(marshaller, "non-null marshaller required"); 120 | Assert.notNull(unmarshaller, "non-null unmarshaller required"); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/serializer/SerializableSerializer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.serializer; 16 | 17 | import org.springframework.lang.Nullable; 18 | 19 | import java.io.*; 20 | 21 | /** 22 | * {@link DynamoSerializer} that can read and write {@link Object} that implement {@link Serializable}. 23 | * Note:Null objects are serialized as empty arrays and vice versa. 24 | * 25 | * @author Georg Zimmermann 26 | */ 27 | public class SerializableSerializer implements DynamoSerializer { 28 | 29 | @Override 30 | public byte[] serialize(@Nullable Object object) throws SerializationException { 31 | try (final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 32 | final ObjectOutputStream output = new ObjectOutputStream(buffer)) { 33 | output.writeObject(object); 34 | return buffer.toByteArray(); 35 | } catch (IOException e) { 36 | throw new SerializationException("Cannot serialize value.", e); 37 | } 38 | } 39 | 40 | @Override 41 | public Object deserialize(@Nullable byte[] bytes) throws SerializationException { 42 | if (bytes == null) { 43 | return null; 44 | } 45 | 46 | try (final ByteArrayInputStream buffer = new ByteArrayInputStream(bytes); 47 | final ObjectInputStream output = new ObjectInputStream(buffer)) { 48 | return output.readObject(); 49 | } catch (IOException | ClassNotFoundException e) { 50 | throw new SerializationException("Cannot deserialize the value.", e); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/serializer/SerializationException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.serializer; 16 | 17 | import org.springframework.core.NestedRuntimeException; 18 | 19 | /** 20 | * Generic exception indicating an error on serialization/deserialization. 21 | * 22 | * @author Georg Zimmermann 23 | */ 24 | public class SerializationException extends NestedRuntimeException { 25 | 26 | /** 27 | * Constructs a {@link SerializationException} instance. 28 | * 29 | * @param msg 30 | */ 31 | public SerializationException(String msg) { 32 | super(msg); 33 | } 34 | 35 | /** 36 | * Constructs a {@link SerializationException} instance. 37 | * 38 | * @param msg the detail message. 39 | * @param cause the nested exception. 40 | */ 41 | public SerializationException(String msg, Throwable cause) { 42 | super(msg, cause); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/serializer/SerializationUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.serializer; 16 | 17 | import org.springframework.lang.Nullable; 18 | 19 | public abstract class SerializationUtils { 20 | 21 | private SerializationUtils() { 22 | throw new IllegalStateException("Utility class"); 23 | } 24 | 25 | static boolean isEmpty(@Nullable byte[] data) { 26 | return (data == null || data.length == 0); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/serializer/StringSerializer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dasburo.spring.cache.dynamo.serializer; 17 | 18 | import org.springframework.lang.Nullable; 19 | 20 | import java.nio.charset.Charset; 21 | import java.nio.charset.StandardCharsets; 22 | 23 | /** 24 | * {@link DynamoSerializer} that can read and write {@link String}. 25 | * Note: 26 | * Does not perform any {@literal null} conversion. 27 | * 28 | * @author Georg Zimmermann 29 | */ 30 | public class StringSerializer implements DynamoSerializer { 31 | 32 | private final Charset charset; 33 | 34 | public StringSerializer() { 35 | charset = StandardCharsets.UTF_8; 36 | } 37 | 38 | @Override 39 | public byte[] serialize(@Nullable String string) throws SerializationException { 40 | return (string == null ? null : string.getBytes(charset)); 41 | } 42 | 43 | @Override 44 | public String deserialize(@Nullable byte[] bytes) throws SerializationException { 45 | return (bytes == null ? null : new String(bytes, charset)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/util/ByteUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.dasburo.spring.cache.dynamo.util; 17 | 18 | import org.springframework.util.Assert; 19 | 20 | import java.nio.ByteBuffer; 21 | 22 | public class ByteUtils { 23 | 24 | private ByteUtils() { 25 | throw new IllegalStateException("Utility class"); 26 | } 27 | 28 | /** 29 | * Extract a byte array from {@link ByteBuffer} without consuming it. 30 | * 31 | * @param byteBuffer must not be {@literal null}. 32 | * @return Return byte[] 33 | * @deprecated By using {@literal SdkBytes} this is not necessary anymore and will be removed in future versions. 34 | */ 35 | @Deprecated 36 | public static byte[] getBytes(ByteBuffer byteBuffer) { 37 | Assert.notNull(byteBuffer, "ByteBuffer must not be null!"); 38 | 39 | ByteBuffer duplicate = byteBuffer.duplicate(); 40 | byte[] bytes = new byte[duplicate.remaining()]; 41 | duplicate.get(bytes); 42 | return bytes; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/dasburo/spring/cache/dynamo/util/TableUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.util; 16 | 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | import software.amazon.awssdk.services.dynamodb.DynamoDbClient; 20 | import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; 21 | import software.amazon.awssdk.services.dynamodb.model.ResourceInUseException; 22 | 23 | /** 24 | * Utility methods for working with DynamoDB tables. 25 | */ 26 | public class TableUtils { 27 | private static final Logger LOGGER = LoggerFactory.getLogger(TableUtils.class); 28 | 29 | private TableUtils() { 30 | throw new IllegalStateException("Utility class"); 31 | } 32 | 33 | /** 34 | * Creates the table if possible and ignores any errors if it already exists. 35 | * 36 | * @param dynamoTemplate The {@literal DynamoDbClient} to use. 37 | * @param createTableRequest The create table request. 38 | * @return True if created, false otherwise. 39 | */ 40 | public static boolean createTableIfNotExists(final DynamoDbClient dynamoTemplate, final CreateTableRequest createTableRequest) { 41 | try { 42 | dynamoTemplate.createTable(createTableRequest); 43 | return true; 44 | } catch (final ResourceInUseException e) { 45 | if (LOGGER.isTraceEnabled()) { 46 | LOGGER.trace("Table " + createTableRequest.tableName() + " already exists", e); 47 | } 48 | } 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.dasburo.spring.cache.dynamo.autoconfigure.DynamoCacheAutoConfiguration -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/DynamoCacheManagerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo; 16 | 17 | import org.junit.jupiter.api.BeforeEach; 18 | import org.junit.jupiter.api.Test; 19 | import org.junit.jupiter.api.extension.ExtendWith; 20 | import org.springframework.beans.factory.annotation.Autowired; 21 | import org.springframework.cache.Cache; 22 | import org.springframework.test.context.ContextConfiguration; 23 | import org.springframework.test.context.junit.jupiter.SpringExtension; 24 | import software.amazon.awssdk.services.dynamodb.DynamoDbClient; 25 | 26 | import java.util.ArrayList; 27 | import java.util.Collection; 28 | import java.util.Collections; 29 | 30 | import static org.hamcrest.CoreMatchers.hasItem; 31 | import static org.hamcrest.CoreMatchers.instanceOf; 32 | import static org.hamcrest.MatcherAssert.assertThat; 33 | import static org.junit.jupiter.api.Assertions.*; 34 | 35 | /** 36 | * Unit tests for {@link DynamoCacheManager}. 37 | * 38 | * @author Georg Zimmermann 39 | */ 40 | @ExtendWith({SpringExtension.class, TestDbCreationExtension.class}) 41 | @ContextConfiguration(classes = TestConfiguration.class) 42 | public class DynamoCacheManagerTest { 43 | 44 | private static final String CACHE_NAME = "cache"; 45 | 46 | @Autowired 47 | private DynamoDbClient dynamoTemplate; 48 | 49 | private DynamoCacheManager manager; 50 | 51 | @BeforeEach 52 | public void setup() { 53 | DynamoCacheBuilder defaultCacheBuilder = DynamoCacheBuilder.newInstance(CACHE_NAME, dynamoTemplate); 54 | this.manager = new DynamoCacheManager(Collections.singletonList(defaultCacheBuilder)); 55 | manager.afterPropertiesSet(); 56 | } 57 | 58 | /** 59 | * Test for {@link DynamoCacheManager#loadCaches()} 60 | */ 61 | @Test 62 | public void loadCaches() { 63 | final Collection initialCaches = new ArrayList<>(); 64 | final DynamoCacheBuilder cache = DynamoCacheBuilder.newInstance("cache", dynamoTemplate); 65 | initialCaches.add(cache); 66 | 67 | final DynamoCacheManager manager = new DynamoCacheManager(initialCaches); 68 | final Collection caches = manager.loadCaches(); 69 | assertNotNull(caches); 70 | assertEquals(1, caches.size()); 71 | } 72 | 73 | /** 74 | * Test for {@link DynamoCacheManager#getCache(String)} 75 | */ 76 | @Test 77 | public void cache() { 78 | final Cache cache = manager.getCache(CACHE_NAME); 79 | assertNotNull(cache); 80 | assertEquals(CACHE_NAME, cache.getName()); 81 | assertThat(cache, instanceOf(DynamoCache.class)); 82 | 83 | final DynamoCache dynamoCache = (DynamoCache) cache; 84 | assertEquals(dynamoCache.getNativeCache(), dynamoTemplate); 85 | } 86 | 87 | /** 88 | * Test for {@link DynamoCacheManager#getCache(String)} 89 | */ 90 | @Test 91 | public void getCache() { 92 | final Cache cache = manager.getCache(CACHE_NAME); 93 | assertNotNull(cache); 94 | 95 | final Cache invalidCache = manager.getCache("invalid"); 96 | assertNull(invalidCache); 97 | } 98 | 99 | /** 100 | * Test for {@link DynamoCacheManager#getCacheNames()} 101 | */ 102 | @Test 103 | public void getCacheNames() { 104 | assertNotNull(manager.getCacheNames()); 105 | assertEquals(1, manager.getCacheNames().size()); 106 | assertThat(manager.getCacheNames(), hasItem(CACHE_NAME)); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/DynamoCacheTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo; 16 | 17 | import com.dasburo.spring.cache.dynamo.helper.Address; 18 | import com.dasburo.spring.cache.dynamo.rootattribute.RootAttributeConfig; 19 | import com.dasburo.spring.cache.dynamo.serializer.DynamoSerializer; 20 | import com.dasburo.spring.cache.dynamo.serializer.Jackson2JsonSerializer; 21 | import com.dasburo.spring.cache.dynamo.serializer.StringSerializer; 22 | import org.junit.jupiter.api.BeforeEach; 23 | import org.junit.jupiter.api.Test; 24 | import org.junit.jupiter.api.extension.ExtendWith; 25 | import org.springframework.beans.factory.annotation.Autowired; 26 | import org.springframework.cache.Cache; 27 | import org.springframework.test.context.ContextConfiguration; 28 | import org.springframework.test.context.junit.jupiter.SpringExtension; 29 | import software.amazon.awssdk.services.dynamodb.DynamoDbClient; 30 | import software.amazon.awssdk.services.dynamodb.model.AttributeValue; 31 | import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; 32 | import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; 33 | 34 | import java.time.Duration; 35 | import java.util.HashMap; 36 | 37 | import static java.util.Collections.singletonList; 38 | import static org.junit.jupiter.api.Assertions.*; 39 | import static org.mockito.Mockito.*; 40 | import static software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType.S; 41 | 42 | /** 43 | * Unit tests for {@link DynamoCache}. 44 | * 45 | * @author Georg Zimmermann 46 | */ 47 | @ExtendWith({SpringExtension.class, TestDbCreationExtension.class}) 48 | @ContextConfiguration(classes = TestConfiguration.class) 49 | public class DynamoCacheTest { 50 | 51 | private static final String CACHE_NAME = "cache"; 52 | private static final Duration TTL = Duration.ofSeconds(10); 53 | 54 | @Autowired 55 | private DynamoCacheWriter writer; 56 | 57 | @Autowired 58 | private DynamoDbClient ddbClient; 59 | 60 | private DynamoCache cache; 61 | 62 | private DynamoSerializer serializer; 63 | 64 | @BeforeEach 65 | public void setup() { 66 | writer = spy(writer); 67 | serializer = new StringSerializer(); 68 | 69 | DynamoCacheConfiguration config = DynamoCacheConfiguration.defaultCacheConfig(); 70 | config.setTtl(TTL); 71 | config.setFlushOnBoot(true); 72 | config.setSerializer(serializer); 73 | 74 | cache = new DynamoCache(CACHE_NAME, writer, config); 75 | reset(writer); 76 | } 77 | 78 | /** 79 | * Test for {@link DynamoCache#DynamoCache(String, DynamoCacheWriter)}. 80 | */ 81 | @Test 82 | public void constructor() { 83 | final String cacheName = cache.getName(); 84 | assertEquals(CACHE_NAME, cacheName); 85 | 86 | final Object nativeCache = cache.getNativeCache(); 87 | assertEquals(nativeCache, writer.getNativeCacheWriter()); 88 | 89 | final DynamoCache dynamoCache = new DynamoCache(CACHE_NAME, writer); 90 | assertNotEquals(0, dynamoCache.getTtl()); 91 | } 92 | 93 | /** 94 | * Test for {@link DynamoCache#clear()}. 95 | */ 96 | @Test 97 | public void clear() { 98 | cache.clear(); 99 | verify(writer, times(1)).clear(cache.getName()); 100 | } 101 | 102 | /** 103 | * Test for {@link DynamoCache#getName()}. 104 | */ 105 | @Test 106 | public void getName() { 107 | final String name = cache.getName(); 108 | assertEquals(CACHE_NAME, name); 109 | } 110 | 111 | /** 112 | * Test for {@link DynamoCache#getNativeCache()}. 113 | */ 114 | @Test 115 | public void getNativeCache() { 116 | final Object nativeCache = cache.getNativeCache(); 117 | assertEquals(nativeCache, writer.getNativeCacheWriter()); 118 | } 119 | 120 | /** 121 | * Test for {@link DynamoCache#getTtl()}. 122 | */ 123 | @Test 124 | public void getTtl() { 125 | final Duration ttl = cache.getTtl(); 126 | assertEquals(TTL, ttl); 127 | } 128 | 129 | /** 130 | * Test for {@link DynamoCache#getWriter()}. 131 | */ 132 | @Test 133 | public void getCacheWriter() { 134 | final Object cacheWriter = cache.getWriter(); 135 | assertEquals(writer, cacheWriter); 136 | } 137 | 138 | /** 139 | * Test for {@link DynamoCache#getSerializer()}. 140 | */ 141 | @Test 142 | public void getSerializer() { 143 | final DynamoSerializer serializer = cache.getSerializer(); 144 | assertEquals(this.serializer, serializer); 145 | } 146 | 147 | /** 148 | * Test for {@link DynamoCache#get(Object)}. 149 | */ 150 | @Test 151 | public void get() { 152 | final String key = "key"; 153 | final String value = "value"; 154 | 155 | cache.put(key, value); 156 | Cache.ValueWrapper wrapper = cache.get(key); 157 | 158 | assertNotNull(wrapper); 159 | assertNotNull(wrapper.get(), value); 160 | } 161 | 162 | /** 163 | * Test for {@link DynamoCache#get(Object)}. 164 | */ 165 | @Test 166 | public void getNullValue() { 167 | final String key = "key"; 168 | final String value = null; 169 | 170 | cache.put(key, value); 171 | 172 | final Cache.ValueWrapper wrapper = cache.get(key); 173 | assertNotNull(wrapper); 174 | assertNull(wrapper.get()); 175 | } 176 | 177 | /** 178 | * Test for {@link DynamoCache#get{T}(Object)}. 179 | */ 180 | @Test 181 | public void getWithCast() { 182 | assertThrows(IllegalStateException.class, () -> { 183 | 184 | final String key = "key"; 185 | final String value = "value"; 186 | 187 | cache.put(key, value); 188 | cache.get(key, Double.class); 189 | }); 190 | } 191 | 192 | /** 193 | * Test for {@link DynamoCache#get{T}(Object)}. 194 | */ 195 | @Test 196 | public void getNullValueWithCast() { 197 | final String key = "key"; 198 | final String value = null; 199 | 200 | cache.put(key, value); 201 | 202 | String valueInCache = cache.get(key, String.class); 203 | assertEquals(value, valueInCache); 204 | 205 | valueInCache = cache.get("key1", String.class); 206 | assertNull(valueInCache); 207 | 208 | cache.get(key, Double.class); 209 | } 210 | 211 | /** 212 | * Test for {@link DynamoCache#evict(Object)}. 213 | */ 214 | @Test 215 | public void evict() { 216 | final String key = "evictKey"; 217 | 218 | cache.put(key, "value"); 219 | cache.evict(key); 220 | 221 | final Cache.ValueWrapper wrapper = cache.get(key); 222 | assertNull(wrapper); 223 | } 224 | 225 | /** 226 | * Test for {@link DynamoCache#put(Object, Object)}. 227 | */ 228 | @Test 229 | public void putShouldAddEntry() { 230 | final String key = "key"; 231 | final String value = "value"; 232 | 233 | cache.put(key, value); 234 | assertNotNull(cache.get(key)); 235 | assertEquals(value, cache.get(key).get()); 236 | } 237 | 238 | /** 239 | * Test for {@link DynamoCache#put(Object, Object)} with {@literal null} value. 240 | */ 241 | @Test 242 | public void putNullShouldAddNullValueEntry() { 243 | final String key = "key"; 244 | final String value = null; 245 | 246 | cache.put(key, value); 247 | assertNotNull(cache.get(key)); 248 | assertEquals(value, cache.get(key).get()); 249 | } 250 | 251 | /** 252 | * Test for {@link DynamoCache#putIfAbsent(Object, Object)}. 253 | */ 254 | @Test 255 | public void putIfAbsentShouldAddEntryIfNotExists() { 256 | final String key = "key"; 257 | final String value = "value"; 258 | 259 | Cache.ValueWrapper wrapper = cache.putIfAbsent(key, value); 260 | 261 | assertNull(wrapper); 262 | assertNotNull(cache.get(key)); 263 | assertEquals(value, cache.get(key).get()); 264 | } 265 | 266 | /** 267 | * Test for {@link DynamoCache#putIfAbsent(Object, Object)}. 268 | */ 269 | @Test 270 | public void putIfAbsentWithNullShouldAddNullValueEntryIfNotExists() { 271 | final String key = "key"; 272 | final String value = null; 273 | 274 | Cache.ValueWrapper wrapper = cache.putIfAbsent(key, value); 275 | 276 | assertNull(wrapper); 277 | assertNotNull(cache.get(key)); 278 | assertEquals(value, cache.get(key).get()); 279 | } 280 | 281 | /** 282 | * Test for {@link DynamoCache#putIfAbsent(Object, Object)}. 283 | */ 284 | @Test 285 | public void putIfAbsentShouldReturnExistingEntryIfNotExists() { 286 | final String key = "key"; 287 | final String value = "value"; 288 | 289 | cache.put(key, value); 290 | 291 | Cache.ValueWrapper wrapper = cache.putIfAbsent(key, value); 292 | 293 | assertNotNull(wrapper); 294 | assertEquals(value, wrapper.get()); 295 | } 296 | 297 | @Test 298 | public void getWithCallableShouldResolveValueIfNotPresent() { 299 | final String key = "key"; 300 | 301 | Address address = new Address("someStreet", 1); 302 | DynamoCacheConfiguration config = DynamoCacheConfiguration.defaultCacheConfig(); 303 | config.setSerializer(new Jackson2JsonSerializer<>(Address.class)); 304 | 305 | Cache addressCache = new DynamoCache(CACHE_NAME, writer, config); 306 | 307 | assertEquals(address, addressCache.get(key, () -> address)); 308 | } 309 | 310 | @Test 311 | public void getWithCallableShouldNotResolveValueIfPresent() { 312 | final String key = "key"; 313 | 314 | Address address = new Address("someStreet", 1); 315 | DynamoCacheConfiguration config = DynamoCacheConfiguration.defaultCacheConfig(); 316 | config.setSerializer(new Jackson2JsonSerializer<>(Address.class)); 317 | 318 | Cache addressCache = new DynamoCache(CACHE_NAME, writer, config); 319 | 320 | addressCache.put(key, address); 321 | 322 | addressCache.get(key, () -> { 323 | throw new IllegalStateException("Why call the value loader when we've got a cache entry?"); 324 | }); 325 | 326 | assertNotNull(addressCache.get(key)); 327 | assertEquals(address, addressCache.get(key).get()); 328 | } 329 | 330 | @Test 331 | public void putWithRootAttributeConfigDoesNotInfluenceTheCoreFunctionality() { 332 | //given 333 | final String itemKey = "key"; 334 | final RootAttributeConfig streetRootAttribute = new RootAttributeConfig("street", S); 335 | Address address = new Address("someStreet", 1); 336 | 337 | DynamoCacheConfiguration config = DynamoCacheConfiguration.defaultCacheConfig(); 338 | config.setSerializer(new Jackson2JsonSerializer<>(Address.class)); 339 | config.setRootAttributes(singletonList(streetRootAttribute)); 340 | 341 | Cache addressCache = new DynamoCache(CACHE_NAME, writer, config); 342 | 343 | //when 344 | addressCache.put(itemKey, address); 345 | 346 | //then 347 | assertNotNull(addressCache.get(itemKey)); 348 | assertEquals(address, addressCache.get(itemKey).get()); 349 | } 350 | 351 | @Test 352 | public void putWithRootAttributePersistsAdditionalRootAttributeOnDynamoDbTable() { 353 | //given 354 | final String itemKey = "key"; 355 | final RootAttributeConfig streetRootAttribute = new RootAttributeConfig("street", S); 356 | Address address = new Address("someStreet", 1); 357 | 358 | DynamoCacheConfiguration config = DynamoCacheConfiguration.defaultCacheConfig(); 359 | config.setSerializer(new Jackson2JsonSerializer<>(Address.class)); 360 | config.setRootAttributes(singletonList(streetRootAttribute)); 361 | 362 | Cache addressCache = new DynamoCache(CACHE_NAME, writer, config); 363 | 364 | //when 365 | addressCache.put(itemKey, address); 366 | 367 | //then 368 | HashMap ddbKey = new HashMap<>(); 369 | ddbKey.put(DefaultDynamoCacheWriter.ATTRIBUTE_KEY, AttributeValue.fromS(itemKey)); 370 | 371 | GetItemResponse ddbItem = ddbClient.getItem(GetItemRequest.builder() 372 | .tableName(CACHE_NAME) 373 | .key(ddbKey) 374 | .build()); 375 | AttributeValue storedRootAttribute = ddbItem.item().get(streetRootAttribute.getName()); 376 | 377 | assertEquals(address.getStreet(), storedRootAttribute.s()); 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/TestConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo; 16 | 17 | import org.springframework.context.annotation.Bean; 18 | import org.springframework.context.annotation.Configuration; 19 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; 20 | import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; 21 | import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; 22 | import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; 23 | import software.amazon.awssdk.http.apache.ApacheHttpClient; 24 | import software.amazon.awssdk.http.apache.ProxyConfiguration; 25 | import software.amazon.awssdk.regions.Region; 26 | import software.amazon.awssdk.services.dynamodb.DynamoDbClient; 27 | 28 | import java.net.URI; 29 | import java.time.Duration; 30 | import java.time.temporal.ChronoUnit; 31 | 32 | /** 33 | * Spring Configuration for basic integration tests. 34 | * 35 | * @author Georg Zimmermann 36 | */ 37 | @Configuration 38 | public class TestConfiguration { 39 | 40 | private static final String ENDPOINT = "http://localhost:8090"; 41 | 42 | @Bean 43 | public AwsCredentialsProvider awsCredentialsProvider() { 44 | return StaticCredentialsProvider.create( 45 | AwsBasicCredentials.create("accessKey", "secretKey")); 46 | } 47 | 48 | /** 49 | * Gets a {@link DynamoDbClient} instance. 50 | * 51 | * @return the {@link DynamoDbClient} instance. 52 | */ 53 | @Bean 54 | public DynamoDbClient amazonDynamoDB(AwsCredentialsProvider awsCredentialsProvider) { 55 | ProxyConfiguration.Builder proxyConfig = ProxyConfiguration.builder(); 56 | 57 | ApacheHttpClient.Builder httpClientBuilder = 58 | ApacheHttpClient.builder() 59 | .proxyConfiguration(proxyConfig.build()) 60 | .connectionTimeout(Duration.of(5, ChronoUnit.SECONDS)); 61 | 62 | ClientOverrideConfiguration.Builder overrideConfig = 63 | ClientOverrideConfiguration.builder(); 64 | 65 | return DynamoDbClient.builder() 66 | .credentialsProvider(awsCredentialsProvider) 67 | .httpClientBuilder(httpClientBuilder) 68 | .overrideConfiguration(overrideConfig.build()) 69 | .endpointOverride(URI.create(ENDPOINT)) 70 | .region(Region.EU_CENTRAL_1) 71 | .build(); 72 | } 73 | 74 | @Bean 75 | public DynamoCacheWriter dynamoCacheWriter(DynamoDbClient amazonDynamoDB) { 76 | return DynamoCacheWriter.lockingDynamoCacheWriter(amazonDynamoDB); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/TestDbCreationExtension.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo; 16 | 17 | import com.amazonaws.services.dynamodbv2.local.main.ServerRunner; 18 | import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer; 19 | import org.junit.jupiter.api.extension.AfterAllCallback; 20 | import org.junit.jupiter.api.extension.BeforeAllCallback; 21 | import org.junit.jupiter.api.extension.ExtensionContext; 22 | 23 | public class TestDbCreationExtension implements AfterAllCallback, BeforeAllCallback { 24 | private DynamoDBProxyServer server; 25 | 26 | public TestDbCreationExtension() { 27 | System.setProperty("sqlite4java.library.path", "native-libs"); 28 | } 29 | 30 | @Override 31 | public void afterAll(ExtensionContext extensionContext) throws Exception { 32 | this.stopUnchecked(server); 33 | } 34 | 35 | @Override 36 | public void beforeAll(ExtensionContext extensionContext) throws Exception { 37 | server = ServerRunner.createServerFromCommandLineArgs( 38 | new String[]{"-inMemory", "-port", "8090"}); 39 | server.start(); 40 | } 41 | 42 | private void stopUnchecked(DynamoDBProxyServer dynamoDbServer) { 43 | try { 44 | dynamoDbServer.stop(); 45 | } catch (Exception e) { 46 | throw new RuntimeException(e); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/UnitTestBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo; 16 | 17 | import org.junit.jupiter.api.AfterEach; 18 | import org.springframework.boot.test.util.TestPropertyValues; 19 | import org.springframework.context.annotation.AnnotationConfigApplicationContext; 20 | 21 | import static org.junit.jupiter.api.Assertions.assertNotNull; 22 | 23 | /** 24 | * Unit test base. 25 | * 26 | * @author Georg Zimmermann 27 | */ 28 | public class UnitTestBase { 29 | 30 | protected AnnotationConfigApplicationContext context; 31 | 32 | @AfterEach 33 | public void close() { 34 | if (context != null) { 35 | context.close(); 36 | } 37 | } 38 | 39 | protected void assertBeanExists(Class bean) { 40 | assertNotNull(context.containsBean(bean.getName()), "The bean does not exist in the context."); 41 | } 42 | 43 | protected AnnotationConfigApplicationContext load(Class[] configs, String... environment) { 44 | // Creates an instance of the "AnnotationConfigApplicationContext" class that represents 45 | // the application context. 46 | AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); 47 | 48 | // Adds environment. 49 | TestPropertyValues.of(environment).applyTo(applicationContext); 50 | 51 | // Registers the configuration class and auto-configuration classes. 52 | applicationContext.register(TestConfiguration.class); 53 | applicationContext.register(configs); 54 | applicationContext.refresh(); 55 | 56 | return applicationContext; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/autoconfigure/DynamoCacheAutoConfigurationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.autoconfigure; 16 | 17 | import com.dasburo.spring.cache.dynamo.DynamoCache; 18 | import com.dasburo.spring.cache.dynamo.DynamoCacheManager; 19 | import com.dasburo.spring.cache.dynamo.TestConfiguration; 20 | import com.dasburo.spring.cache.dynamo.TestDbCreationExtension; 21 | import com.dasburo.spring.cache.dynamo.UnitTestBase; 22 | import com.dasburo.spring.cache.dynamo.rootattribute.RootAttributeConfig; 23 | import org.junit.jupiter.api.BeforeEach; 24 | import org.junit.jupiter.api.Test; 25 | import org.junit.jupiter.api.extension.ExtendWith; 26 | import org.springframework.test.context.ContextConfiguration; 27 | 28 | import java.time.Duration; 29 | import java.util.Collections; 30 | import java.util.List; 31 | 32 | import static org.junit.jupiter.api.Assertions.assertEquals; 33 | import static org.junit.jupiter.api.Assertions.assertNotNull; 34 | import static software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType.S; 35 | 36 | /** 37 | * Unit tests for {@code DynamoCacheAutoConfiguration} class. 38 | * 39 | * @author Georg Zimmermann 40 | */ 41 | @ExtendWith(TestDbCreationExtension.class) 42 | @ContextConfiguration(classes = TestConfiguration.class) 43 | public class DynamoCacheAutoConfigurationTest extends UnitTestBase { 44 | 45 | private static final String CACHE_NAME = "cache"; 46 | private static final boolean FLUSH_ON_BOOT = false; 47 | private static final Duration TTL = Duration.ofSeconds(30); 48 | private static final Long READ_CAPACITY_UNITS = 1L; 49 | private static final Long WRITE_CAPACITY_UNITS = 1L; 50 | private static final List ROOT_ATTRIBUTES = Collections.singletonList(new RootAttributeConfig("street", S)); 51 | 52 | /** 53 | * Executes before the execution of tests. 54 | */ 55 | @BeforeEach 56 | public void load() { 57 | context = load( 58 | new Class[]{DynamoCacheAutoConfiguration.class}, 59 | "spring.cache.dynamo.caches[0].ttl:" + TTL, 60 | "spring.cache.dynamo.caches[0].cacheName:" + CACHE_NAME, 61 | "spring.cache.dynamo.caches[0].flushOnBoot:" + FLUSH_ON_BOOT, 62 | "spring.cache.dynamo.caches[0].readCapacityUnits:" + READ_CAPACITY_UNITS, 63 | "spring.cache.dynamo.caches[0].writeCapacityUnits:" + WRITE_CAPACITY_UNITS, 64 | "spring.cache.dynamo.caches[0].rootAttributes[0].name:" + ROOT_ATTRIBUTES.get(0).getName(), 65 | "spring.cache.dynamo.caches[0].rootAttributes[0].type:" + ROOT_ATTRIBUTES.get(0).getType().name() 66 | ); 67 | } 68 | 69 | /** 70 | * Test for using of {@code DynamoCacheManager} instance. 71 | */ 72 | @Test 73 | public void testInstance() { 74 | load(); 75 | assertBeanExists(DynamoCache.class); 76 | 77 | final DynamoCacheManager manager = context.getBean(DynamoCacheManager.class); 78 | assertNotNull(manager); 79 | } 80 | 81 | /** 82 | * Test for properties of {@code DynamoCacheManager} instance. 83 | */ 84 | @Test 85 | public void testProperties() { 86 | load(); 87 | assertBeanExists(DynamoCacheManager.class); 88 | 89 | final DynamoCacheManager manager = context.getBean(DynamoCacheManager.class); 90 | assertNotNull(manager); 91 | 92 | final DynamoCache cache = (DynamoCache) manager.getCache(CACHE_NAME); 93 | assertNotNull(cache); 94 | assertEquals(TTL, cache.getTtl()); 95 | assertEquals(CACHE_NAME, cache.getName()); 96 | assertEquals(FLUSH_ON_BOOT, cache.isFlushOnBoot()); 97 | assertEquals(ROOT_ATTRIBUTES.get(0).getName(), cache.getRootAttributes().get(0).getName()); 98 | assertEquals(ROOT_ATTRIBUTES.get(0).getType(), cache.getRootAttributes().get(0).getType()); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/helper/Address.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.helper; 16 | 17 | import java.io.Serializable; 18 | 19 | /** 20 | * Simple Serializable Test Class representing an Address. 21 | * 22 | * @author Georg Zimmermann 23 | */ 24 | public class Address implements Serializable { 25 | 26 | private static final long serialVersionUID = 1L; 27 | 28 | private String street; 29 | 30 | private Integer number; 31 | 32 | public Address() {} 33 | 34 | public Address(String street, int number) { 35 | super(); 36 | this.street = street; 37 | this.number = number; 38 | } 39 | 40 | public String getStreet() { 41 | return street; 42 | } 43 | 44 | public void setStreet(String street) { 45 | this.street = street; 46 | } 47 | 48 | public Integer getNumber() { 49 | return number; 50 | } 51 | 52 | public void setNumber(Integer number) { 53 | this.number = number; 54 | } 55 | 56 | public int hashCode() { 57 | final int prime = 31; 58 | int result = 1; 59 | result = prime * result + ((number == null) ? 0 : number.hashCode()); 60 | result = prime * result + ((street == null) ? 0 : street.hashCode()); 61 | return result; 62 | } 63 | 64 | public boolean equals(Object obj) { 65 | if (this == obj) 66 | return true; 67 | if (obj == null) 68 | return false; 69 | if (!(obj instanceof Address)) 70 | return false; 71 | Address other = (Address) obj; 72 | if (number == null) { 73 | if (other.number != null) 74 | return false; 75 | } else if (!number.equals(other.number)) 76 | return false; 77 | if (street == null) { 78 | return other.street == null; 79 | } else return street.equals(other.street); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/helper/Company.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.helper; 16 | 17 | import java.io.Serializable; 18 | 19 | /** 20 | * Simple Serializable Test Class representing a Company. 21 | * 22 | * @author Georg Zimmermann 23 | */ 24 | public class Company implements Serializable { 25 | private static final long serialVersionUID = 1L; 26 | 27 | private String name; 28 | private String industry; 29 | 30 | private Integer established; 31 | private Address address; 32 | 33 | public Company() {} 34 | 35 | public Company(String name, String industry, int established) { 36 | this(name, industry, established, null); 37 | } 38 | 39 | public Company(String name, String industry, int established, Address address) { 40 | super(); 41 | this.name = name; 42 | this.industry = industry; 43 | this.established = established; 44 | this.address = address; 45 | } 46 | 47 | public String getName() { 48 | return name; 49 | } 50 | 51 | public void setName(String name) { 52 | this.name = name; 53 | } 54 | 55 | public String getIndustry() { 56 | return industry; 57 | } 58 | 59 | public void setIndustry(String industry) { 60 | this.industry = industry; 61 | } 62 | 63 | public int getEstablished() { 64 | return established; 65 | } 66 | 67 | public void setEstablished(int established) { 68 | this.established = established; 69 | } 70 | 71 | public Address getAddress() { 72 | return address; 73 | } 74 | 75 | public void setAddress(Address address) { 76 | this.address = address; 77 | } 78 | 79 | public int hashCode() { 80 | final int prime = 31; 81 | int result = 1; 82 | result = prime * result + ((address == null) ? 0 : address.hashCode()); 83 | result = prime * result + ((established == null) ? 0 : established.hashCode()); 84 | result = prime * result + ((name == null) ? 0 : name.hashCode()); 85 | result = prime * result + ((industry == null) ? 0 : industry.hashCode()); 86 | return result; 87 | } 88 | 89 | public boolean equals(Object obj) { 90 | if (this == obj) 91 | return true; 92 | if (obj == null) 93 | return false; 94 | if (!(obj instanceof Company)) 95 | return false; 96 | Company other = (Company) obj; 97 | if (address == null) { 98 | if (other.address != null) 99 | return false; 100 | } else if (!address.equals(other.address)) 101 | return false; 102 | if (established == null) { 103 | if (other.established != null) 104 | return false; 105 | } else if (!established.equals(other.established)) 106 | return false; 107 | if (name == null) { 108 | if (other.name != null) 109 | return false; 110 | } else if (!name.equals(other.name)) 111 | return false; 112 | if (industry == null) { 113 | return other.industry == null; 114 | } else return industry.equals(other.industry); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/helper/NotSerializeable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.helper; 16 | 17 | public class NotSerializeable { 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/rootattribute/RootAttributeConfigTest.java: -------------------------------------------------------------------------------- 1 | package com.dasburo.spring.cache.dynamo.rootattribute; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.dasburo.spring.cache.dynamo.DefaultDynamoCacheWriter.*; 6 | import static org.junit.jupiter.api.Assertions.assertThrows; 7 | import static software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType.S; 8 | 9 | public class RootAttributeConfigTest { 10 | 11 | @Test 12 | public void RootAttributeConfigTest_TypeMustNotBeNull() { 13 | assertThrows(IllegalArgumentException.class, () -> 14 | new RootAttributeConfig("sampleField", null) 15 | ); 16 | } 17 | 18 | @Test 19 | public void RootAttributeConfigTest_NameMustNotBeNull() { 20 | assertThrows(IllegalArgumentException.class, () -> 21 | new RootAttributeConfig(null, S) 22 | ); 23 | } 24 | 25 | @Test 26 | public void RootAttributeConfigTest_NameMustNotBeEmpty() { 27 | assertThrows(IllegalArgumentException.class, () -> 28 | new RootAttributeConfig("", S) 29 | ); 30 | } 31 | 32 | @Test 33 | public void RootAttributeConfigTest_NameMustNotEqualATTRIBUTE_KEY() { 34 | assertThrows(IllegalArgumentException.class, () -> 35 | new RootAttributeConfig(ATTRIBUTE_KEY, S) 36 | ); 37 | } 38 | 39 | @Test 40 | public void RootAttributeConfigTest_NameMustNotEqualATTRIBUTE_VALUE() { 41 | assertThrows(IllegalArgumentException.class, () -> 42 | new RootAttributeConfig(ATTRIBUTE_VALUE, S) 43 | ); 44 | } 45 | 46 | @Test 47 | public void RootAttributeConfigTest_NameMustNotEqualATTRIBUTE_TTL() { 48 | assertThrows(IllegalArgumentException.class, () -> 49 | new RootAttributeConfig(ATTRIBUTE_TTL, S) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/rootattribute/RootAttributeReaderTest.java: -------------------------------------------------------------------------------- 1 | package com.dasburo.spring.cache.dynamo.rootattribute; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import software.amazon.awssdk.core.SdkBytes; 6 | import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; 7 | 8 | import java.nio.ByteBuffer; 9 | import java.util.HashMap; 10 | import java.util.LinkedHashMap; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertNull; 14 | import static software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType.S; 15 | 16 | public class RootAttributeReaderTest { 17 | 18 | private RootAttributeReader rootAttributeReader; 19 | 20 | @BeforeEach 21 | public void setup() { 22 | rootAttributeReader = new RootAttributeReader(); 23 | } 24 | 25 | @Test 26 | public void testRootAttributeReader_StringFieldCanBeHandled() { 27 | //given 28 | RootAttributeConfig rootAttributeConfig = new RootAttributeConfig("stringField", S); 29 | SampleTestClass sampleInstance = new SampleTestClass(); 30 | sampleInstance.setStringField("dummy-value"); 31 | 32 | //when 33 | RootAttribute rootAttribute = rootAttributeReader.readRootAttribute(rootAttributeConfig, sampleInstance); 34 | 35 | //then 36 | assertEquals("stringField", rootAttribute.getName()); 37 | assertEquals("dummy-value", rootAttribute.getAttributeValue().s()); 38 | } 39 | 40 | @Test 41 | public void testRootAttributeReader_BooleanFieldCanBeHandledAsString() { 42 | //given 43 | RootAttributeConfig rootAttributeConfig = new RootAttributeConfig("booleanField", S); 44 | SampleTestClass sampleInstance = new SampleTestClass(); 45 | sampleInstance.setBooleanField(true); 46 | 47 | //when 48 | RootAttribute rootAttribute = rootAttributeReader.readRootAttribute(rootAttributeConfig, sampleInstance); 49 | 50 | //then 51 | assertEquals("booleanField", rootAttribute.getName()); 52 | assertEquals("true", rootAttribute.getAttributeValue().s()); 53 | } 54 | 55 | @Test 56 | public void testRootAttributeReader_EnumFieldCanBeHandledAsString() { 57 | //given 58 | RootAttributeConfig rootAttributeConfig = new RootAttributeConfig("enumField", ScalarAttributeType.N); 59 | SampleTestClass sampleInstance = new SampleTestClass(); 60 | sampleInstance.setEnumField(S); 61 | 62 | //when 63 | RootAttribute rootAttribute = rootAttributeReader.readRootAttribute(rootAttributeConfig, sampleInstance); 64 | 65 | //then 66 | assertEquals("enumField", rootAttribute.getName()); 67 | assertEquals("S", rootAttribute.getAttributeValue().n()); 68 | } 69 | 70 | @Test 71 | public void testRootAttributeReader_IntegerFieldCanBeHandled() { 72 | //given 73 | RootAttributeConfig rootAttributeConfig = new RootAttributeConfig("integerField", ScalarAttributeType.N); 74 | SampleTestClass sampleInstance = new SampleTestClass(); 75 | sampleInstance.setIntegerField(123); 76 | 77 | //when 78 | RootAttribute rootAttribute = rootAttributeReader.readRootAttribute(rootAttributeConfig, sampleInstance); 79 | 80 | //then 81 | assertEquals("integerField", rootAttribute.getName()); 82 | assertEquals("123", rootAttribute.getAttributeValue().n()); 83 | } 84 | 85 | @Test 86 | public void testRootAttributeReader_DoubleFieldCanBeHandled() { 87 | //given 88 | RootAttributeConfig rootAttributeConfig = new RootAttributeConfig("doubleField", ScalarAttributeType.N); 89 | SampleTestClass sampleInstance = new SampleTestClass(); 90 | sampleInstance.setDoubleField(123.0); 91 | 92 | //when 93 | RootAttribute rootAttribute = rootAttributeReader.readRootAttribute(rootAttributeConfig, sampleInstance); 94 | 95 | //then 96 | assertEquals("doubleField", rootAttribute.getName()); 97 | assertEquals("123.0", rootAttribute.getAttributeValue().n()); 98 | } 99 | 100 | @Test 101 | public void testRootAttributeReader_ByteBufferFieldCanBeHandled() { 102 | //given 103 | RootAttributeConfig rootAttributeConfig = new RootAttributeConfig("byteBufferField", ScalarAttributeType.B); 104 | SampleTestClass sampleInstance = new SampleTestClass(); 105 | sampleInstance.setByteBufferField(ByteBuffer.wrap("dummy-text".getBytes())); 106 | 107 | //when 108 | RootAttribute rootAttribute = rootAttributeReader.readRootAttribute(rootAttributeConfig, sampleInstance); 109 | 110 | //then 111 | assertEquals("byteBufferField", rootAttribute.getName()); 112 | assertEquals(SdkBytes.fromByteArray("dummy-text".getBytes()), rootAttribute.getAttributeValue().b()); 113 | } 114 | 115 | @Test 116 | public void testRootAttributeReader_HashMapCanBeHandled() { 117 | //given 118 | RootAttributeConfig rootAttributeConfig = new RootAttributeConfig("stringField", S); 119 | HashMap map = new HashMap<>(); 120 | map.put("stringField", "dummy-text"); 121 | 122 | //when 123 | RootAttribute rootAttribute = rootAttributeReader.readRootAttribute(rootAttributeConfig, map); 124 | 125 | //then 126 | assertEquals("stringField", rootAttribute.getName()); 127 | assertEquals("dummy-text", rootAttribute.getAttributeValue().s()); 128 | } 129 | 130 | @Test 131 | public void testRootAttributeReader_LinkedHashMapCanBeHandled() { 132 | //given 133 | RootAttributeConfig rootAttributeConfig = new RootAttributeConfig("stringField", S); 134 | LinkedHashMap map = new LinkedHashMap<>(); 135 | map.put("stringField", "dummy-text"); 136 | 137 | //when 138 | RootAttribute rootAttribute = rootAttributeReader.readRootAttribute(rootAttributeConfig, map); 139 | 140 | //then 141 | assertEquals("stringField", rootAttribute.getName()); 142 | assertEquals("dummy-text", rootAttribute.getAttributeValue().s()); 143 | } 144 | 145 | @Test 146 | public void testRootAttributeReader_UnknownFieldIsGetsIgnored() { 147 | //given 148 | RootAttributeConfig rootAttributeConfig = new RootAttributeConfig("unknownField", S); 149 | SampleTestClass sampleInstance = new SampleTestClass(); 150 | 151 | //when 152 | RootAttribute rootAttribute = rootAttributeReader.readRootAttribute(rootAttributeConfig, sampleInstance); 153 | 154 | //then 155 | assertNull(rootAttribute); 156 | } 157 | 158 | @Test 159 | public void testRootAttributeReader_NullValueOfKnownFieldsGetsIgnored() { 160 | //given 161 | RootAttributeConfig rootAttributeConfig = new RootAttributeConfig("stringField", S); 162 | SampleTestClass sampleInstance = new SampleTestClass(); 163 | sampleInstance.setStringField(null); 164 | 165 | //when 166 | RootAttribute rootAttribute = rootAttributeReader.readRootAttribute(rootAttributeConfig, sampleInstance); 167 | 168 | //then 169 | assertNull(rootAttribute); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/rootattribute/SampleTestClass.java: -------------------------------------------------------------------------------- 1 | package com.dasburo.spring.cache.dynamo.rootattribute; 2 | 3 | import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; 4 | 5 | import java.nio.ByteBuffer; 6 | 7 | public class SampleTestClass { 8 | 9 | private String stringField; 10 | private Boolean booleanField; 11 | private ScalarAttributeType enumField; 12 | private Integer integerField; 13 | private Double doubleField; 14 | private ByteBuffer byteBufferField; 15 | 16 | public String getStringField() { 17 | return stringField; 18 | } 19 | 20 | public void setStringField(String stringField) { 21 | this.stringField = stringField; 22 | } 23 | 24 | public Boolean getBooleanField() { 25 | return booleanField; 26 | } 27 | 28 | public void setBooleanField(Boolean booleanField) { 29 | this.booleanField = booleanField; 30 | } 31 | 32 | public ScalarAttributeType getEnumField() { 33 | return enumField; 34 | } 35 | 36 | public void setEnumField(ScalarAttributeType enumField) { 37 | this.enumField = enumField; 38 | } 39 | 40 | public Integer getIntegerField() { 41 | return integerField; 42 | } 43 | 44 | public void setIntegerField(Integer integerField) { 45 | this.integerField = integerField; 46 | } 47 | 48 | public Double getDoubleField() { 49 | return doubleField; 50 | } 51 | 52 | public void setDoubleField(Double doubleField) { 53 | this.doubleField = doubleField; 54 | } 55 | 56 | public ByteBuffer getByteBufferField() { 57 | return byteBufferField; 58 | } 59 | 60 | public void setByteBufferField(ByteBuffer byteBufferField) { 61 | this.byteBufferField = byteBufferField; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/serializer/GZipSerializerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.serializer; 16 | 17 | import org.junit.jupiter.api.BeforeEach; 18 | import org.junit.jupiter.api.Test; 19 | 20 | import static org.junit.jupiter.api.Assertions.assertEquals; 21 | import static org.junit.jupiter.api.Assertions.assertNull; 22 | 23 | public class GZipSerializerTest { 24 | 25 | private GZipSerializer serializer; 26 | 27 | @BeforeEach 28 | public void setup() { 29 | serializer = new GZipSerializer<>(new StringSerializer()); 30 | } 31 | 32 | @Test 33 | public void testGZipSerializer() { 34 | assertEquals("test", serializer.deserialize(serializer.serialize("test"))); 35 | } 36 | 37 | @Test 38 | public void testGZipSerializer_ShouldReturnNullWhenSerializingNull() { 39 | assertNull(serializer.deserialize(serializer.serialize(null))); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/serializer/Jackson2JsonSerializerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.serializer; 16 | 17 | import com.dasburo.spring.cache.dynamo.helper.Address; 18 | import com.dasburo.spring.cache.dynamo.helper.Company; 19 | import com.dasburo.spring.cache.dynamo.helper.NotSerializeable; 20 | import com.fasterxml.jackson.core.JsonProcessingException; 21 | import com.fasterxml.jackson.databind.ObjectMapper; 22 | import org.junit.jupiter.api.Assertions; 23 | import org.junit.jupiter.api.BeforeEach; 24 | import org.junit.jupiter.api.Test; 25 | 26 | import java.util.Arrays; 27 | 28 | import static org.junit.jupiter.api.Assertions.assertEquals; 29 | import static org.junit.jupiter.api.Assertions.assertNull; 30 | import static org.mockito.ArgumentMatchers.any; 31 | import static org.mockito.Mockito.*; 32 | 33 | public class Jackson2JsonSerializerTest { 34 | 35 | private Jackson2JsonSerializer serializer; 36 | 37 | @BeforeEach 38 | public void setup() { 39 | this.serializer = new Jackson2JsonSerializer<>(Company.class); 40 | } 41 | 42 | @Test 43 | public void testJackson2JsonSerializer() { 44 | Company company = new Company("company", "IT", 2019, new Address("street", 1)); 45 | assertEquals(company, serializer.deserialize(serializer.serialize(company))); 46 | } 47 | 48 | @Test 49 | public void testJackson2JsonSerializer_ShouldReturnNullWhenSerializingNull() { 50 | assertEquals(null, serializer.serialize(null)); 51 | } 52 | 53 | @Test 54 | public void testJackson2JsonSerializer_ShouldReturnNullWhenDeserializingEmptyByteArray() { 55 | assertNull(serializer.deserialize(new byte[0])); 56 | } 57 | 58 | @Test 59 | public void testSerializableSerializer_ShouldThrowExceptionWhenSerializingNonSerializableObject() { 60 | Assertions.assertThrows(SerializationException.class, () -> 61 | serializer.serialize(new NotSerializeable()) 62 | ); 63 | } 64 | 65 | @Test 66 | public void testJackson2JsonSerializer_ShouldThrowExceptionWhenDeserializingInvalidByteArray() { 67 | Company company = new Company("company", "IT", 2019, new Address("street", 1)); 68 | byte[] serializedValue = serializer.serialize(company); 69 | Arrays.sort(serializedValue); // corrupt serialization result 70 | 71 | Assertions.assertThrows(SerializationException.class, () -> 72 | serializer.deserialize(serializedValue) 73 | ); 74 | } 75 | 76 | @Test 77 | public void testJackson2JsonSerializer_ThrowsExceptionWhenSettingNullObjectMapper() { 78 | Assertions.assertThrows(IllegalArgumentException.class, () -> 79 | serializer.setObjectMapper(null) 80 | ); 81 | } 82 | 83 | @Test 84 | public void testJackson2JsonSerializer_ShouldUSeCustomObjectMapper() throws JsonProcessingException { 85 | ObjectMapper mockObjMapper = mock(ObjectMapper.class); 86 | serializer.setObjectMapper(mockObjMapper); 87 | 88 | serializer.serialize(new Address()); 89 | verify(mockObjMapper, times(1)).writeValueAsBytes(any()); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/serializer/OxmSerializerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.serializer; 16 | 17 | import com.dasburo.spring.cache.dynamo.helper.Address; 18 | import org.junit.jupiter.api.BeforeEach; 19 | import org.junit.jupiter.api.Test; 20 | import org.mockito.Mock; 21 | import org.springframework.oxm.Marshaller; 22 | import org.springframework.oxm.Unmarshaller; 23 | import org.springframework.oxm.jaxb.Jaxb2Marshaller; 24 | 25 | import static org.junit.jupiter.api.Assertions.assertNull; 26 | import static org.junit.jupiter.api.Assertions.assertThrows; 27 | 28 | public class OxmSerializerTest { 29 | 30 | @Mock 31 | private Marshaller marshaller; 32 | 33 | @Mock 34 | private Unmarshaller unmarshaller; 35 | 36 | OxmSerializer sut; 37 | 38 | @BeforeEach 39 | public void setup() { 40 | Jaxb2Marshaller m = new Jaxb2Marshaller(); 41 | sut = new OxmSerializer(m, m); 42 | } 43 | 44 | @Test 45 | public void testOxmSerializer_WithNullValueShouldReturnNullObject() { 46 | assertNull(sut.serialize(null)); 47 | } 48 | 49 | @Test 50 | public void testOxmSerializer_WithWrongValueShouldThrowException() { 51 | assertThrows(SerializationException.class, () -> { 52 | Address test = new Address(); 53 | sut.serialize(test); 54 | }); 55 | } 56 | 57 | @Test 58 | public void testOxmSerializer_WithNullValueShouldReturnNullObjectWhenDeserializing() { 59 | assertNull(sut.deserialize(null)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/serializer/SerializableSerializerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.serializer; 16 | 17 | import com.dasburo.spring.cache.dynamo.helper.NotSerializeable; 18 | import org.junit.jupiter.api.BeforeEach; 19 | import org.junit.jupiter.api.Test; 20 | 21 | import static org.junit.jupiter.api.Assertions.*; 22 | 23 | public class SerializableSerializerTest { 24 | 25 | private SerializableSerializer serializer; 26 | 27 | @BeforeEach 28 | public void setup() { 29 | serializer = new SerializableSerializer(); 30 | } 31 | 32 | @Test 33 | public void testSerializableSerializer() { 34 | assertEquals("test", serializer.deserialize(serializer.serialize("test"))); 35 | } 36 | 37 | @Test 38 | public void testSerializableSerializer_ShouldReturnNullWhenDeSerializingNullObject() { 39 | assertNull(serializer.deserialize(null)); 40 | } 41 | 42 | @Test 43 | public void testSerializableSerializer_ShouldReturnNullWhenSerializingNull() { 44 | assertNull(serializer.deserialize(serializer.serialize(null))); 45 | } 46 | 47 | @Test 48 | public void testSerializableSerializer_ShouldThrowExceptionWhenSerializingNonSerializableObject() { 49 | assertThrows(SerializationException.class, () -> 50 | serializer.serialize(new NotSerializeable()) 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/serializer/SerializationUtilsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.serializer; 16 | 17 | import org.junit.jupiter.api.Test; 18 | 19 | import static org.junit.jupiter.api.Assertions.assertTrue; 20 | 21 | public class SerializationUtilsTest { 22 | 23 | @Test 24 | public void isEmptyOnNullTest() { 25 | assertTrue(SerializationUtils.isEmpty(null)); 26 | } 27 | 28 | @Test 29 | public void isEmptyOnEmptyArrayTest() { 30 | assertTrue(SerializationUtils.isEmpty(new byte[0])); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/serializer/StringSerializerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 com.dasburo.spring.cache.dynamo.serializer; 16 | 17 | import org.junit.jupiter.api.BeforeEach; 18 | import org.junit.jupiter.api.Test; 19 | 20 | import static org.junit.jupiter.api.Assertions.assertEquals; 21 | 22 | public class StringSerializerTest { 23 | 24 | private StringSerializer serializer; 25 | 26 | @BeforeEach 27 | public void setup() { 28 | serializer = new StringSerializer(); 29 | } 30 | 31 | @Test 32 | public void serialize() { 33 | String expectedValue = "test"; 34 | 35 | byte[] serializedValue = serializer.serialize(expectedValue); 36 | String actualValue = serializer.deserialize(serializedValue); 37 | 38 | assertEquals(expectedValue, actualValue); 39 | } 40 | 41 | @Test 42 | public void serializeWithNull() { 43 | String expectedValue = null; 44 | 45 | byte[] serializedValue = serializer.serialize(expectedValue); 46 | Object actualValue = serializer.deserialize(serializedValue); 47 | 48 | assertEquals(expectedValue, actualValue); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/dasburo/spring/cache/dynamo/util/TableUtilsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * https://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 | 16 | package com.dasburo.spring.cache.dynamo.util; 17 | 18 | import org.junit.jupiter.api.Test; 19 | import org.junit.jupiter.api.extension.ExtendWith; 20 | import org.mockito.Mock; 21 | import org.mockito.junit.jupiter.MockitoExtension; 22 | import software.amazon.awssdk.services.dynamodb.DynamoDbClient; 23 | import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; 24 | import software.amazon.awssdk.services.dynamodb.model.ResourceInUseException; 25 | 26 | import static org.junit.jupiter.api.Assertions.assertFalse; 27 | import static org.mockito.ArgumentMatchers.any; 28 | import static org.mockito.Mockito.when; 29 | 30 | @ExtendWith(MockitoExtension.class) 31 | class TableUtilsTest { 32 | 33 | @Mock 34 | private DynamoDbClient mockDynamoTemplate; 35 | 36 | @Test 37 | public void testCreateTableIfNotExists_alreadyExists() { 38 | when(mockDynamoTemplate.createTable(any(CreateTableRequest.class))) 39 | .thenThrow(ResourceInUseException.class); 40 | 41 | boolean result = TableUtils.createTableIfNotExists(mockDynamoTemplate, CreateTableRequest.builder().build()); 42 | assertFalse(result); 43 | } 44 | } 45 | --------------------------------------------------------------------------------